From 987fca6aa65eb3489851e5eee5adc9f78ae5f49f Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:14 +0800 Subject: [PATCH 01/35] chore: drop Dify upstream submodule Co-Authored-By: Claude Opus 5 (1M context) --- .gitmodules | 3 --- dify | 1 - 2 files changed, 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 dify diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ed27fed..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "dify"] - path = dify - url = https://github.com/langgenius/dify.git diff --git a/dify b/dify deleted file mode 160000 index aa71784..0000000 --- a/dify +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aa71784627fafe252e8bb246e7abed38171e48b3 From ec5896ea8ece78bd454de703a5dacbfe37a99e58 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:16 +0800 Subject: [PATCH 02/35] build: upgrade dependencies and add quality tooling Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 54 +- pytest.ini | 3 + requirements.txt | 3616 ++++++++++++++++++++++++++-------------------- uv.lock | 3583 ++++++++++++++++++++++++++------------------- 4 files changed, 4214 insertions(+), 3042 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0cae10a..3f45b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,39 +1,55 @@ [project] name = "pyrag-kit" -version = "1.3.0" +version = "1.4.0" description = "A lightweight Python implementation of Dify core RAG logic" readme = "README.md" authors = [{ name = "Mison", email = "1360962086@qq.com" }] license = { text = "MIT" } requires-python = ">=3.11" dependencies = [ - "anthropic>=0.86.0", - "faiss-cpu>=1.13.2", - "google-genai>=1.68.0", + "anthropic>=1.4.0", + "faiss-cpu>=1.15.0", + "google-genai>=2.22.0", "httpx>=0.28.1", "jieba>=0.42.1", - "langchain>=1.2.13", - "langchain-community>=0.4.1", - "langchain-core>=1.2.20", - "langchain-text-splitters>=1.1.1", - "numpy>=2.4.3", - "openai>=2.29.0", + "langchain>=1.4.0", + "langchain-community>=0.4.2", + "langchain-core>=1.6.2", + "langchain-text-splitters>=1.1.2", + "numpy>=2.4.6", + "openai>=3.10.0", "openpyxl>=3.1.5", - "prompt-toolkit>=3.0.52", - "pydantic>=2.12.5", - "pydantic-settings>=2.13.1", + "prompt-toolkit>=3.0.53", + "pydantic>=2.13.5", + "pydantic-settings>=2.15.0", "pyfiglet>=1.0.4", "questionary>=2.1.1", "rank-bm25>=0.2.2", - "requests>=2.32.5", - "rich>=14.3.3", + "requests>=2.34.2", + "rich>=15.0.0", "tenacity>=9.1.4", - "tiktoken>=0.12.0", - "volcengine-python-sdk[ark]>=5.0.18", + "tiktoken>=0.14.0", + "volcengine-python-sdk[ark]>=5.0.48", ] [dependency-groups] dev = [ - "pyinstaller>=6.16.0", - "pytest>=9.0.2", + "bandit>=1.9.4", + "mypy>=2.3.1", + "pyinstaller>=6.22.2", + "pytest>=9.1.1", + "ruff>=0.16.6", ] + +[tool.ruff.lint] +# Provider and configuration boundaries intentionally expose ValueError for +# malformed values as part of the existing public contract. +ignore = ["TRY004"] + +[tool.mypy] +# jieba does not publish typing metadata; the application boundary keeps its +# use isolated in main.py, so missing third-party stubs should not hide errors +# in the repository's own modules. +[[tool.mypy.overrides]] +module = ["jieba", "jieba.posseg"] +ignore_missing_imports = true diff --git a/pytest.ini b/pytest.ini index 109f387..c501f07 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] pythonpath = . testpaths = tests +# langsmith is a transitive dependency of langchain and its pytest plugin +# imports the full tracing SDK during collection; PyRAG-Kit does not use it. +addopts = -p no:langsmith # 忽略第三方库的 DeprecationWarning 和 UserWarning filterwarnings = ignore::DeprecationWarning diff --git a/requirements.txt b/requirements.txt index f7b92f2..361ccb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,361 +1,528 @@ # This file was autogenerated by uv via the following command: -# uv export --format requirements-txt --output-file requirements.txt -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv export --format requirements.txt --output-file requirements.txt --locked +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.3 \ - --hash=sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf \ - --hash=sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c \ - --hash=sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c \ - --hash=sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423 \ - --hash=sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f \ - --hash=sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2 \ - --hash=sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf \ - --hash=sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64 \ - --hash=sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998 \ - --hash=sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d \ - --hash=sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea \ - --hash=sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463 \ - --hash=sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4 \ - --hash=sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767 \ - --hash=sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43 \ - --hash=sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592 \ - --hash=sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a \ - --hash=sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687 \ - --hash=sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8 \ - --hash=sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261 \ - --hash=sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a \ - --hash=sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4 \ - --hash=sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587 \ - --hash=sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91 \ - --hash=sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 \ - --hash=sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344 \ - --hash=sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6 \ - --hash=sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3 \ - --hash=sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29 \ - --hash=sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3 \ - --hash=sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b \ - --hash=sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51 \ - --hash=sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c \ - --hash=sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926 \ - --hash=sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64 \ - --hash=sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f \ - --hash=sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b \ - --hash=sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e \ - --hash=sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440 \ - --hash=sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6 \ - --hash=sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d \ - --hash=sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415 \ - --hash=sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce \ - --hash=sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603 \ - --hash=sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0 \ - --hash=sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf \ - --hash=sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591 \ - --hash=sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540 \ - --hash=sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26 \ - --hash=sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a \ - --hash=sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a \ - --hash=sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9 \ - --hash=sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba \ - --hash=sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df \ - --hash=sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679 \ - --hash=sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc \ - --hash=sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29 \ - --hash=sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984 \ - --hash=sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1 \ - --hash=sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632 \ - --hash=sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56 \ - --hash=sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239 \ - --hash=sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168 \ - --hash=sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88 \ - --hash=sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc \ - --hash=sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046 \ - --hash=sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0 \ - --hash=sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3 \ - --hash=sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1 \ - --hash=sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c \ - --hash=sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25 \ - --hash=sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033 \ - --hash=sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1 \ - --hash=sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d \ - --hash=sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f \ - --hash=sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f \ - --hash=sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29 \ - --hash=sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72 \ - --hash=sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57 \ - --hash=sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c \ - --hash=sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808 \ - --hash=sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7 \ - --hash=sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 \ - --hash=sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3 \ - --hash=sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730 \ - --hash=sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via langchain-community aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +altgraph==0.17.5 \ + --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ + --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 + # via + # macholib + # pyinstaller +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anthropic==0.86.0 \ - --hash=sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5 \ - --hash=sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57 +anthropic==1.4.0 \ + --hash=sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c \ + --hash=sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c # via pyrag-kit -anyio==4.12.1 \ - --hash=sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703 \ - --hash=sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c +anyio==4.15.1 \ + --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \ + --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94 # via # anthropic + # google-genai # httpx + # httpx2 + # langsmith # openai + # volcengine-python-sdk +ast-serialize==0.9.0 \ + --hash=sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295 \ + --hash=sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099 \ + --hash=sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3 \ + --hash=sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d \ + --hash=sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2 \ + --hash=sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3 \ + --hash=sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd \ + --hash=sha256:1ed5824b4b2fa37ad93fa2db23d8243a3d315b3e2d7ca70f99b2727d8788af05 \ + --hash=sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b \ + --hash=sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550 \ + --hash=sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7 \ + --hash=sha256:24de2bc930b7e1ca86641136b9875a1e5f80f52b484deddc67893eeaf9077bd9 \ + --hash=sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e \ + --hash=sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24 \ + --hash=sha256:322282ac5337e5e776bc416f2b5201e680fa4dacf92ea739bd35e46a28a66c41 \ + --hash=sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48 \ + --hash=sha256:3ee40752ddb4fb5c6a67161d13f3ce3df7987dcb9272260542a86b0be1519ae2 \ + --hash=sha256:41da4332492222d56345d5e436eed4fbec76caadee959f6ffa3cd2fc1bd51895 \ + --hash=sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf \ + --hash=sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b \ + --hash=sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167 \ + --hash=sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c \ + --hash=sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321 \ + --hash=sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb \ + --hash=sha256:5fc57f17fb4ce49b4eeccfcd2670e4a55659bd740bb8e8aedbe511ccab8b5f03 \ + --hash=sha256:62a96e327e2a178d6c295b10422e95c992a9286f0ec1b2bc7cd5b4873252a38c \ + --hash=sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008 \ + --hash=sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c \ + --hash=sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a \ + --hash=sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7 \ + --hash=sha256:7ac7b4cdf89ca8318aae157d824017596784982851c8a89a621973f261574696 \ + --hash=sha256:7e9f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7 \ + --hash=sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7 \ + --hash=sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb \ + --hash=sha256:95dcb93f30258dcf09d9b6a302dac9323b70e9df8c18ee0bf4ea1fa7cc5f1875 \ + --hash=sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25 \ + --hash=sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0 \ + --hash=sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2b4c3da4 \ + --hash=sha256:ae1c46eb97865823f9843c4b80145e011874923e1a4a44b45738a5309d83e9f5 \ + --hash=sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47 \ + --hash=sha256:af082cb7e6c4fa3a428aa616c13d709a944076eba84a73184de15621cc1a915d \ + --hash=sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec \ + --hash=sha256:b559cffac5a71a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e \ + --hash=sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10 \ + --hash=sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063 \ + --hash=sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b \ + --hash=sha256:c55010ea0fafc6bc8231809d328bda781bfe41a01c43522be5eb3713fd855cda \ + --hash=sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82 \ + --hash=sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09 \ + --hash=sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b \ + --hash=sha256:d9c635eacfc02b91da6796d3b5ff9086e511b8a29b19f9b3f4f978b8d170f838 \ + --hash=sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d \ + --hash=sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f \ + --hash=sha256:ded5ed06ec469407d6cd571ace7a7a25809cc388e4bac1dd35c7747469fc7fdf \ + --hash=sha256:e3c69f2ce565c786bd3853ce42495e8a63610516f79651c7cb4f2cd0ddfaee52 \ + --hash=sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa \ + --hash=sha256:f8c824d822a2ac54ec4228b88c0d170ab0024cf285eeee57b9d4f994003fb553 + # via mypy attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via aiohttp -certifi==2026.2.25 \ - --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ - --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # requests -cffi==2.0.0 ; platform_python_implementation != 'PyPy' \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 + # volcengine-python-sdk +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -charset-normalizer==3.4.6 \ - --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ - --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ - --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ - --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ - --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ - --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ - --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ - --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ - --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ - --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ - --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ - --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ - --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ - --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ - --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ - --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ - --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ - --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ - --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ - --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ - --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ - --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ - --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ - --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ - --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ - --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ - --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ - --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ - --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ - --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ - --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ - --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ - --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ - --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ - --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ - --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ - --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ - --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ - --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ - --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ - --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ - --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ - --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ - --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ - --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ - --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ - --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ - --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ - --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ - --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ - --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ - --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ - --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ - --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ - --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ - --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ - --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ - --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ - --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ - --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ - --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ - --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ - --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ - --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ - --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ - --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ - --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ - --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ - --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ - --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ - --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ - --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ - --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ - --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ - --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ - --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ - --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ - --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ - --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ - --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ - --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ - --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 +charset-normalizer==3.5.1 \ + --hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \ + --hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \ + --hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \ + --hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \ + --hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \ + --hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \ + --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \ + --hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \ + --hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \ + --hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \ + --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \ + --hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \ + --hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \ + --hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \ + --hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \ + --hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \ + --hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \ + --hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \ + --hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \ + --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \ + --hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \ + --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \ + --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \ + --hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \ + --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \ + --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \ + --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \ + --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \ + --hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \ + --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \ + --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \ + --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \ + --hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \ + --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \ + --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \ + --hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \ + --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \ + --hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \ + --hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \ + --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \ + --hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \ + --hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \ + --hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \ + --hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \ + --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \ + --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \ + --hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \ + --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \ + --hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \ + --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \ + --hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \ + --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \ + --hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \ + --hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \ + --hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \ + --hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \ + --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \ + --hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \ + --hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \ + --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \ + --hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \ + --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \ + --hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \ + --hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \ + --hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \ + --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \ + --hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \ + --hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \ + --hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \ + --hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \ + --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \ + --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \ + --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \ + --hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \ + --hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \ + --hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \ + --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \ + --hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \ + --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \ + --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \ + --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \ + --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \ + --hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \ + --hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \ + --hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \ + --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \ + --hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \ + --hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \ + --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \ + --hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \ + --hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \ + --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \ + --hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \ + --hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \ + --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \ + --hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \ + --hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \ + --hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \ + --hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \ + --hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \ + --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \ + --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \ + --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \ + --hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \ + --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \ + --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \ + --hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \ + --hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \ + --hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \ + --hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \ + --hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \ + --hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \ + --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \ + --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \ + --hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \ + --hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \ + --hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \ + --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \ + --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \ + --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \ + --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \ + --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \ + --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \ + --hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \ + --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \ + --hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \ + --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \ + --hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \ + --hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \ + --hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \ + --hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \ + --hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \ + --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \ + --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \ + --hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \ + --hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \ + --hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \ + --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \ + --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \ + --hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via tqdm -cryptography==46.0.5 \ - --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ - --hash=sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235 \ - --hash=sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9 \ - --hash=sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356 \ - --hash=sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257 \ - --hash=sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad \ - --hash=sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4 \ - --hash=sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c \ - --hash=sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614 \ - --hash=sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed \ - --hash=sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31 \ - --hash=sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229 \ - --hash=sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0 \ - --hash=sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731 \ - --hash=sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b \ - --hash=sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4 \ - --hash=sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4 \ - --hash=sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 \ - --hash=sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595 \ - --hash=sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1 \ - --hash=sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678 \ - --hash=sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48 \ - --hash=sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76 \ - --hash=sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0 \ - --hash=sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18 \ - --hash=sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d \ - --hash=sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d \ - --hash=sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1 \ - --hash=sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981 \ - --hash=sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7 \ - --hash=sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82 \ - --hash=sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2 \ - --hash=sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4 \ - --hash=sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663 \ - --hash=sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c \ - --hash=sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d \ - --hash=sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a \ - --hash=sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a \ - --hash=sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d \ - --hash=sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b \ - --hash=sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a \ - --hash=sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826 \ - --hash=sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee \ - --hash=sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9 \ - --hash=sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648 \ - --hash=sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da \ - --hash=sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2 \ - --hash=sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2 \ - --hash=sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87 - # via google-auth -dataclasses-json==0.6.7 \ - --hash=sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a \ - --hash=sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0 - # via langchain-community + # via + # bandit + # pytest +cryptography==50.0.1 \ + --hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \ + --hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \ + --hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \ + --hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \ + --hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \ + --hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \ + --hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \ + --hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \ + --hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \ + --hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \ + --hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \ + --hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \ + --hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \ + --hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \ + --hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \ + --hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \ + --hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \ + --hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \ + --hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \ + --hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \ + --hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \ + --hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \ + --hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \ + --hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \ + --hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \ + --hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \ + --hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \ + --hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \ + --hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \ + --hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \ + --hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \ + --hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \ + --hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \ + --hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \ + --hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \ + --hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \ + --hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \ + --hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \ + --hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \ + --hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \ + --hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \ + --hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \ + --hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \ + --hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \ + --hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \ + --hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef + # via + # google-auth + # volcengine-python-sdk distro==1.9.0 \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 # via - # anthropic - # openai -docstring-parser==0.17.0 \ - --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ - --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 + # google-genai + # langsmith +docstring-parser==0.18.0 \ + --hash=sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015 \ + --hash=sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b # via anthropic et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -faiss-cpu==1.13.2 \ - --hash=sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1 \ - --hash=sha256:1243967eeb2298791ff7f3683a4abd2100d7e6ec7542ca05c3b75d47a7f621e5 \ - --hash=sha256:19508a1badfb36e456c1c8664eeb948349f604db5c7545f277a0126b4a84b080 \ - --hash=sha256:2c4f696ae76e7c97cbc12311db83aaf1e7f4f7be06a3ffea7e5b0e8ec1fd805b \ - --hash=sha256:7c5944d7807d58fe7244b6aba06be710ee7ed99343365ed92699349efe979f51 \ - --hash=sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c \ - --hash=sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f \ - --hash=sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f \ - --hash=sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1 \ - --hash=sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7 \ - --hash=sha256:b82c01d30430dd7b1fa442001b9099735d1a82f6bb72033acdc9206d5ac66a64 \ - --hash=sha256:c8b645e7d56591aa35dc75415bb53a62e4a494dba010e16f4b67daeffd830bd7 \ - --hash=sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317 \ - --hash=sha256:cb4b5ee184816a4b099162ac93c0d7f0033d81a88e7c1291d0a9cc41ec348984 +faiss-cpu==1.15.0 \ + --hash=sha256:22dddb013e764aad66dac6cd15b49c7598d60339e0591b73b5e081629419c21b \ + --hash=sha256:30da3029952f0de69f16ce31946fd63fc3e292c867749bbcd2c0a0f09fd06f65 \ + --hash=sha256:37170d5e9ead4b6bfd9c314afc39e17e92064068a0c5a4063dd3f39568c2667e \ + --hash=sha256:50ea471ef1f4f3580eda8ab0ec9727d4bf65fd71c444bf306ce7cdbba8a42b21 \ + --hash=sha256:5b940897b317febaa761088513a3db164fad3ac71a5e1ed7be9a052c9bf1a447 \ + --hash=sha256:5d0a2d5d33fe023e263d0d355a837f20db67578e3be27fc5f4012a273274abf6 \ + --hash=sha256:88fbe1acac6978869063cb2f9477f85718da596a6e0a17751618f9c756bce255 \ + --hash=sha256:90169515a95ea58a9a95d419e518907927a8ef54c46788396365ec5902c9c8df \ + --hash=sha256:dd383bb1ce06fabcff5785f998f253aa88f88dcbe1fe36c922417cd6666dd896 \ + --hash=sha256:ec9b29aae29e428c085c2d49dbb02e4673cdea75db418d420f9e60e0b4184498 # via pyrag-kit frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ @@ -459,280 +626,232 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -google-ai-generativelanguage==0.6.15 \ - --hash=sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c \ - --hash=sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3 - # via google-generativeai -google-api-core==2.25.2 \ - --hash=sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300 \ - --hash=sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7 - # via - # google-ai-generativelanguage - # google-api-python-client - # google-generativeai -google-api-python-client==2.193.0 \ - --hash=sha256:8f88d16e89d11341e0a8b199cafde0fb7e6b44260dffb88d451577cbd1bb5d33 \ - --hash=sha256:c42aa324b822109901cfecab5dc4fc3915d35a7b376835233c916c70610322db - # via google-generativeai -google-auth==2.49.1 \ - --hash=sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64 \ - --hash=sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7 - # via - # google-ai-generativelanguage - # google-api-core - # google-api-python-client - # google-auth-httplib2 - # google-generativeai -google-auth-httplib2==0.3.0 \ - --hash=sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b \ - --hash=sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776 - # via google-api-python-client -google-generativeai==0.8.6 \ - --hash=sha256:37a0eaaa95e5bbf888828e20a4a1b2c196cc9527d194706e58a68ff388aeb0fa +google-auth==2.57.1 \ + --hash=sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9 \ + --hash=sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e + # via google-genai +google-genai==2.22.0 \ + --hash=sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd \ + --hash=sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6 # via pyrag-kit -googleapis-common-protos==1.73.0 \ - --hash=sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a \ - --hash=sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8 - # via - # google-api-core - # grpcio-status -greenlet==3.3.2 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ - --hash=sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd \ - --hash=sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082 \ - --hash=sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b \ - --hash=sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5 \ - --hash=sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f \ - --hash=sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727 \ - --hash=sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e \ - --hash=sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2 \ - --hash=sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f \ - --hash=sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327 \ - --hash=sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd \ - --hash=sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070 \ - --hash=sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99 \ - --hash=sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be \ - --hash=sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79 \ - --hash=sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506 \ - --hash=sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a \ - --hash=sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395 \ - --hash=sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4 \ - --hash=sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492 \ - --hash=sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab \ - --hash=sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 \ - --hash=sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce \ - --hash=sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5 \ - --hash=sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac \ - --hash=sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55 \ - --hash=sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124 \ - --hash=sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 \ - --hash=sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 \ - --hash=sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd \ - --hash=sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4 \ - --hash=sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab \ - --hash=sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff \ - --hash=sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a \ - --hash=sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9 \ - --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 \ - --hash=sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71 \ - --hash=sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92 \ - --hash=sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643 \ - --hash=sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54 +greenlet==3.5.5 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ + --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \ + --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \ + --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \ + --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \ + --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \ + --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \ + --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \ + --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \ + --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \ + --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \ + --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \ + --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \ + --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \ + --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \ + --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \ + --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \ + --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \ + --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \ + --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \ + --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \ + --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \ + --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \ + --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \ + --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \ + --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \ + --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \ + --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \ + --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \ + --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \ + --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \ + --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \ + --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \ + --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \ + --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \ + --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \ + --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \ + --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \ + --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \ + --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \ + --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \ + --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \ + --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \ + --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \ + --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \ + --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \ + --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \ + --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \ + --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \ + --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \ + --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \ + --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \ + --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \ + --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \ + --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \ + --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \ + --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b # via sqlalchemy -grpcio==1.78.0 \ - --hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \ - --hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \ - --hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \ - --hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \ - --hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \ - --hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \ - --hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \ - --hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \ - --hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \ - --hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \ - --hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \ - --hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \ - --hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \ - --hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \ - --hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \ - --hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \ - --hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \ - --hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \ - --hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \ - --hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \ - --hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \ - --hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \ - --hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \ - --hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \ - --hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \ - --hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \ - --hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \ - --hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \ - --hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \ - --hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \ - --hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \ - --hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \ - --hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \ - --hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \ - --hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \ - --hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \ - --hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \ - --hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \ - --hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \ - --hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \ - --hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce - # via - # google-api-core - # grpcio-status -grpcio-status==1.71.2 \ - --hash=sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3 \ - --hash=sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50 - # via google-api-core h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - # via httpcore + # via + # httpcore + # httpcore2 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httplib2==0.31.2 \ - --hash=sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24 \ - --hash=sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349 - # via - # google-api-python-client - # google-auth-httplib2 +httpcore2==2.12.0 ; sys_platform != 'emscripten' \ + --hash=sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb \ + --hash=sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648 + # via httpx2 httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via - # anthropic + # google-genai + # langchain-core # langgraph-sdk - # langsmith - # openai + # pyrag-kit + # volcengine-python-sdk httpx-sse==0.4.3 \ --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d # via langchain-community -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +httpx2==2.12.0 \ + --hash=sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf \ + --hash=sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36 + # via + # anthropic + # langsmith + # openai +httpx2-jsfetch==1.0 ; python_full_version >= '3.12' and sys_platform == 'emscripten' \ + --hash=sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60 \ + --hash=sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32 + # via httpx2 +idna==3.19 \ + --hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \ + --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4 # via # anyio # httpx + # httpx2 # requests # yarl +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest jieba==0.42.1 \ --hash=sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2 # via pyrag-kit -jiter==0.13.0 \ - --hash=sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726 \ - --hash=sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654 \ - --hash=sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d \ - --hash=sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663 \ - --hash=sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8 \ - --hash=sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5 \ - --hash=sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394 \ - --hash=sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad \ - --hash=sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202 \ - --hash=sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1 \ - --hash=sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59 \ - --hash=sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d \ - --hash=sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92 \ - --hash=sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228 \ - --hash=sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf \ - --hash=sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018 \ - --hash=sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6 \ - --hash=sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d \ - --hash=sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024 \ - --hash=sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820 \ - --hash=sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2 \ - --hash=sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72 \ - --hash=sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089 \ - --hash=sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a \ - --hash=sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9 \ - --hash=sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434 \ - --hash=sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4 \ - --hash=sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa \ - --hash=sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0 \ - --hash=sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d \ - --hash=sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0 \ - --hash=sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5 \ - --hash=sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6 \ - --hash=sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911 \ - --hash=sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607 \ - --hash=sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9 \ - --hash=sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d \ - --hash=sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d \ - --hash=sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95 \ - --hash=sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08 \ - --hash=sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19 \ - --hash=sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe \ - --hash=sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09 \ - --hash=sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2 \ - --hash=sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc \ - --hash=sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0 \ - --hash=sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91 \ - --hash=sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663 \ - --hash=sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6 \ - --hash=sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f \ - --hash=sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411 \ - --hash=sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66 \ - --hash=sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59 \ - --hash=sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef \ - --hash=sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68 \ - --hash=sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c \ - --hash=sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b \ - --hash=sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93 \ - --hash=sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df \ - --hash=sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152 \ - --hash=sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701 \ - --hash=sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0 \ - --hash=sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3 \ - --hash=sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2 \ - --hash=sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40 \ - --hash=sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2 \ - --hash=sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939 \ - --hash=sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096 \ - --hash=sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c \ - --hash=sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159 \ - --hash=sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165 \ - --hash=sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f \ - --hash=sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4 \ - --hash=sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a \ - --hash=sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb \ - --hash=sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505 \ - --hash=sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10 \ - --hash=sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f +jiter==0.16.0 \ + --hash=sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536 \ + --hash=sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f \ + --hash=sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3 \ + --hash=sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce \ + --hash=sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e \ + --hash=sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85 \ + --hash=sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7 \ + --hash=sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207 \ + --hash=sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5 \ + --hash=sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056 \ + --hash=sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331 \ + --hash=sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03 \ + --hash=sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93 \ + --hash=sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290 \ + --hash=sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a \ + --hash=sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284 \ + --hash=sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195 \ + --hash=sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730 \ + --hash=sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48 \ + --hash=sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69 \ + --hash=sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274 \ + --hash=sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7 \ + --hash=sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5 \ + --hash=sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106 \ + --hash=sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00 \ + --hash=sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9 \ + --hash=sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b \ + --hash=sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818 \ + --hash=sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c \ + --hash=sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2 \ + --hash=sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077 \ + --hash=sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037 \ + --hash=sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2 \ + --hash=sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84 \ + --hash=sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a \ + --hash=sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a \ + --hash=sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3 \ + --hash=sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb \ + --hash=sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe \ + --hash=sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c \ + --hash=sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad \ + --hash=sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee \ + --hash=sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053 \ + --hash=sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929 \ + --hash=sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450 \ + --hash=sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3 \ + --hash=sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9 \ + --hash=sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734 \ + --hash=sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585 \ + --hash=sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29 \ + --hash=sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91 \ + --hash=sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee \ + --hash=sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e \ + --hash=sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5 \ + --hash=sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db \ + --hash=sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd \ + --hash=sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620 \ + --hash=sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567 \ + --hash=sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898 \ + --hash=sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01 \ + --hash=sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea \ + --hash=sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026 \ + --hash=sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e \ + --hash=sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e \ + --hash=sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8 \ + --hash=sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883 \ + --hash=sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702 \ + --hash=sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09 \ + --hash=sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72 \ + --hash=sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8 \ + --hash=sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af \ + --hash=sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3 \ + --hash=sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf \ + --hash=sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f \ + --hash=sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1 \ + --hash=sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e \ + --hash=sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e \ + --hash=sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127 \ + --hash=sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e # via # anthropic # openai -joblib==1.5.3 \ - --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ - --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 - # via scikit-learn jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via langchain-core -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca # via jsonpatch -langchain==1.2.13 \ - --hash=sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1 \ - --hash=sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4 +langchain==1.4.0 \ + --hash=sha256:08da122a439f738f4c09a5158cfa3d2c1d8bea2d0bde7fbbf4aeb4d7f7fd9d80 \ + --hash=sha256:af1dc0161d30944a52ec7844d9bf890d0497b12ec0703069f3503b4003cbbac3 # via pyrag-kit -langchain-classic==1.0.3 \ - --hash=sha256:168ef1dfbfb18cae5a9ff0accecc9413a5b5aa3464b53fa841561a3384b6324a \ - --hash=sha256:26df1ec9806b1fbff19d9085a747ea7d8d82d7e3fb1d25132859979de627ef79 +langchain-classic==1.0.8 \ + --hash=sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213 \ + --hash=sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484 # via langchain-community -langchain-community==0.4.1 \ - --hash=sha256:2135abb2c7748a35c84613108f7ebf30f8505b18c3c18305ffaecfc7651f6c6a \ - --hash=sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85 +langchain-community==0.4.2 \ + --hash=sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466 \ + --hash=sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e # via pyrag-kit -langchain-core==1.2.20 \ - --hash=sha256:b65ff678f3c3dc1f1b4d03a3af5ee3b8d51f9be5181d74eb53c6c11cd9dd5e68 \ - --hash=sha256:c7ac8b976039b5832abb989fef058b88c270594ba331efc79e835df046e7dc44 +langchain-core==1.6.2 \ + --hash=sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d \ + --hash=sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711 # via # langchain # langchain-classic @@ -741,46 +860,164 @@ langchain-core==1.2.20 \ # langgraph # langgraph-checkpoint # langgraph-prebuilt + # langgraph-sdk # pyrag-kit -langchain-text-splitters==1.1.1 \ - --hash=sha256:34861abe7c07d9e49d4dc852d0129e26b32738b60a74486853ec9b6d6a8e01d2 \ - --hash=sha256:5ed0d7bf314ba925041e7d7d17cd8b10f688300d5415fb26c29442f061e329dc +langchain-protocol==0.0.19 \ + --hash=sha256:4cdf879a492a35980fd859ae792d3c65458ccaae504e183c9a10d7eac1f0720f \ + --hash=sha256:79d90a1425122ac87e8052e2ec054fbd09c3edbf341bdfb6397112a495c7bf8c + # via + # langchain-core + # langgraph-sdk +langchain-text-splitters==1.1.2 \ + --hash=sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627 \ + --hash=sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10 # via # langchain-classic # pyrag-kit -langgraph==1.1.3 \ - --hash=sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546 \ - --hash=sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370 +langgraph==1.2.11 \ + --hash=sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b \ + --hash=sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363 # via langchain -langgraph-checkpoint==4.0.1 \ - --hash=sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9 \ - --hash=sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034 +langgraph-checkpoint==4.2.0 \ + --hash=sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942 \ + --hash=sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7 # via # langgraph # langgraph-prebuilt -langgraph-prebuilt==1.0.8 \ - --hash=sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69 \ - --hash=sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0 +langgraph-prebuilt==1.1.0 \ + --hash=sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528 \ + --hash=sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9 # via langgraph -langgraph-sdk==0.3.12 \ - --hash=sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327 \ - --hash=sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71 +langgraph-sdk==0.4.4 \ + --hash=sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64 \ + --hash=sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b # via langgraph -langsmith==0.7.22 \ - --hash=sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04 \ - --hash=sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4 +langsmith==0.12.2 \ + --hash=sha256:ca2b6386cf452cf8edebf27c9c476503c18bad895e7ad566578393fa0ba7fea2 \ + --hash=sha256:ff369ba4390e0969dbb109e838281404ae637e76ef73b347336bae05b4f87331 # via # langchain-classic # langchain-community # langchain-core -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +librt==0.15.0 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302 \ + --hash=sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad \ + --hash=sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08 \ + --hash=sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785 \ + --hash=sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9 \ + --hash=sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890 \ + --hash=sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2 \ + --hash=sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab \ + --hash=sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13 \ + --hash=sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2 \ + --hash=sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56 \ + --hash=sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b \ + --hash=sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6 \ + --hash=sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d \ + --hash=sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d \ + --hash=sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95 \ + --hash=sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714 \ + --hash=sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416 \ + --hash=sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54 \ + --hash=sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae \ + --hash=sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28 \ + --hash=sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d \ + --hash=sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40 \ + --hash=sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47 \ + --hash=sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19 \ + --hash=sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162 \ + --hash=sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c \ + --hash=sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa \ + --hash=sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc \ + --hash=sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988 \ + --hash=sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f \ + --hash=sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81 \ + --hash=sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d \ + --hash=sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95 \ + --hash=sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605 \ + --hash=sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108 \ + --hash=sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf \ + --hash=sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8 \ + --hash=sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a \ + --hash=sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965 \ + --hash=sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b \ + --hash=sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218 \ + --hash=sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd \ + --hash=sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b \ + --hash=sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d \ + --hash=sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622 \ + --hash=sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022 \ + --hash=sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60 \ + --hash=sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef \ + --hash=sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22 \ + --hash=sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8 \ + --hash=sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb \ + --hash=sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca \ + --hash=sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc \ + --hash=sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239 \ + --hash=sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0 \ + --hash=sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0 \ + --hash=sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa \ + --hash=sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3 \ + --hash=sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0 \ + --hash=sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915 \ + --hash=sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953 \ + --hash=sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65 \ + --hash=sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0 \ + --hash=sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd \ + --hash=sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1 \ + --hash=sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570 \ + --hash=sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c \ + --hash=sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101 \ + --hash=sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801 \ + --hash=sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2 \ + --hash=sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab \ + --hash=sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8 \ + --hash=sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18 \ + --hash=sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162 \ + --hash=sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81 \ + --hash=sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a \ + --hash=sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63 \ + --hash=sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc \ + --hash=sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1 \ + --hash=sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656 \ + --hash=sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d \ + --hash=sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9 \ + --hash=sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad \ + --hash=sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879 \ + --hash=sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d \ + --hash=sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3 \ + --hash=sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328 \ + --hash=sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659 \ + --hash=sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285 \ + --hash=sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26 \ + --hash=sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9 \ + --hash=sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1 \ + --hash=sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6 \ + --hash=sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c \ + --hash=sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38 \ + --hash=sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b \ + --hash=sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e \ + --hash=sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21 \ + --hash=sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374 \ + --hash=sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa \ + --hash=sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8 \ + --hash=sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab \ + --hash=sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15 \ + --hash=sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091 \ + --hash=sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993 \ + --hash=sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db \ + --hash=sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a \ + --hash=sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9 + # via mypy +macholib==1.16.4 ; sys_platform == 'darwin' \ + --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ + --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 + # via pyinstaller +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marshmallow==3.26.2 \ - --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ - --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 - # via dataclasses-json mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -899,161 +1136,272 @@ multidict==6.7.1 \ # via # aiohttp # yarl +mypy==2.3.1 \ + --hash=sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d \ + --hash=sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0 \ + --hash=sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d \ + --hash=sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226 \ + --hash=sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133 \ + --hash=sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc \ + --hash=sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63 \ + --hash=sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3 \ + --hash=sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9 \ + --hash=sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4 \ + --hash=sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561 \ + --hash=sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419 \ + --hash=sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d \ + --hash=sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb \ + --hash=sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b \ + --hash=sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1 \ + --hash=sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523 \ + --hash=sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc \ + --hash=sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4 \ + --hash=sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb \ + --hash=sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57 \ + --hash=sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82 \ + --hash=sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac \ + --hash=sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74 \ + --hash=sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355 \ + --hash=sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080 \ + --hash=sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29 \ + --hash=sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f \ + --hash=sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531 \ + --hash=sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6 \ + --hash=sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff \ + --hash=sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8 \ + --hash=sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045 \ + --hash=sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9 \ + --hash=sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b \ + --hash=sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3 \ + --hash=sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb \ + --hash=sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595 \ + --hash=sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01 \ + --hash=sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021 \ + --hash=sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2 \ + --hash=sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6 \ + --hash=sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e \ + --hash=sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306 \ + --hash=sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8 \ + --hash=sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 - # via typing-inspect -numpy==2.4.3 \ - --hash=sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5 \ - --hash=sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc \ - --hash=sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657 \ - --hash=sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd \ - --hash=sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02 \ - --hash=sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a \ - --hash=sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26 \ - --hash=sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92 \ - --hash=sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22 \ - --hash=sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9 \ - --hash=sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9 \ - --hash=sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9 \ - --hash=sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152 \ - --hash=sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168 \ - --hash=sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e \ - --hash=sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd \ - --hash=sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15 \ - --hash=sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093 \ - --hash=sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5 \ - --hash=sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb \ - --hash=sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b \ - --hash=sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd \ - --hash=sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18 \ - --hash=sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5 \ - --hash=sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950 \ - --hash=sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e \ - --hash=sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a \ - --hash=sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd \ - --hash=sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4 \ - --hash=sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0 \ - --hash=sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef \ - --hash=sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5 \ - --hash=sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24 \ - --hash=sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476 \ - --hash=sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71 \ - --hash=sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc \ - --hash=sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e \ - --hash=sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8 \ - --hash=sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c \ - --hash=sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec \ - --hash=sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875 \ - --hash=sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a \ - --hash=sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79 \ - --hash=sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f \ - --hash=sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147 \ - --hash=sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857 \ - --hash=sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368 \ - --hash=sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720 \ - --hash=sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470 \ - --hash=sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b \ - --hash=sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920 \ - --hash=sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52 \ - --hash=sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73 \ - --hash=sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349 \ - --hash=sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4 \ - --hash=sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3 \ - --hash=sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7 \ - --hash=sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee \ - --hash=sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395 \ - --hash=sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c \ - --hash=sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611 \ - --hash=sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028 \ - --hash=sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d \ - --hash=sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7 \ - --hash=sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070 \ - --hash=sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0 \ - --hash=sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc \ - --hash=sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687 \ - --hash=sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0 \ - --hash=sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f \ - --hash=sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97 \ - --hash=sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67 + # via mypy +numpy==2.4.6 ; python_full_version < '3.12' \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via # faiss-cpu # langchain-community - # pandas # pyrag-kit # rank-bm25 - # scikit-learn - # scipy -openai==2.29.0 \ - --hash=sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec \ - --hash=sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a +numpy==2.5.2 ; python_full_version >= '3.12' \ + --hash=sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a \ + --hash=sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f \ + --hash=sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7 \ + --hash=sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0 \ + --hash=sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3 \ + --hash=sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c \ + --hash=sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce \ + --hash=sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8 \ + --hash=sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1 \ + --hash=sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4 \ + --hash=sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee \ + --hash=sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740 \ + --hash=sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98 \ + --hash=sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710 \ + --hash=sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee \ + --hash=sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68 \ + --hash=sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf \ + --hash=sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8 \ + --hash=sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf \ + --hash=sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b \ + --hash=sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884 \ + --hash=sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03 \ + --hash=sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69 \ + --hash=sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4 \ + --hash=sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842 \ + --hash=sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65 \ + --hash=sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080 \ + --hash=sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e \ + --hash=sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e \ + --hash=sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414 \ + --hash=sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59 \ + --hash=sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8 \ + --hash=sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617 \ + --hash=sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4 \ + --hash=sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb \ + --hash=sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251 \ + --hash=sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d \ + --hash=sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2 \ + --hash=sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab \ + --hash=sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657 \ + --hash=sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15 \ + --hash=sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9 \ + --hash=sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8 \ + --hash=sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323 \ + --hash=sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788 \ + --hash=sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc \ + --hash=sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56 \ + --hash=sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1 \ + --hash=sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d \ + --hash=sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec \ + --hash=sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2 \ + --hash=sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e \ + --hash=sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7 \ + --hash=sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26 \ + --hash=sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514 \ + --hash=sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860 \ + --hash=sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a \ + --hash=sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1 \ + --hash=sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab \ + --hash=sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba \ + --hash=sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12 \ + --hash=sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6 \ + --hash=sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e \ + --hash=sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac \ + --hash=sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb \ + --hash=sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f + # via + # faiss-cpu + # langchain-community + # pyrag-kit + # rank-bm25 +openai==3.10.0 \ + --hash=sha256:c2dc841b944c4720a222f9a92f66a9a122e46968d87ed7857104655054c687ba \ + --hash=sha256:d1f307d866c7d12872809aeb68b7d1a085b49c2656787eae7c39ba381a41ad0d # via pyrag-kit openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 # via pyrag-kit -orjson==3.11.7 \ - --hash=sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e \ - --hash=sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f \ - --hash=sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8 \ - --hash=sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733 \ - --hash=sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223 \ - --hash=sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d \ - --hash=sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650 \ - --hash=sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5 \ - --hash=sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1 \ - --hash=sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8 \ - --hash=sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3 \ - --hash=sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6 \ - --hash=sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910 \ - --hash=sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2 \ - --hash=sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d \ - --hash=sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a \ - --hash=sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5 \ - --hash=sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e \ - --hash=sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471 \ - --hash=sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892 \ - --hash=sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3 \ - --hash=sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b \ - --hash=sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504 \ - --hash=sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539 \ - --hash=sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785 \ - --hash=sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1 \ - --hash=sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab \ - --hash=sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576 \ - --hash=sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b \ - --hash=sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141 \ - --hash=sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62 \ - --hash=sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c \ - --hash=sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2 \ - --hash=sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b \ - --hash=sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49 \ - --hash=sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960 \ - --hash=sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705 \ - --hash=sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace \ - --hash=sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b \ - --hash=sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1 \ - --hash=sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561 \ - --hash=sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157 \ - --hash=sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10 \ - --hash=sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5 \ - --hash=sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757 \ - --hash=sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d \ - --hash=sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f \ - --hash=sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf \ - --hash=sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183 \ - --hash=sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74 \ - --hash=sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0 \ - --hash=sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e \ - --hash=sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d \ - --hash=sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539 \ - --hash=sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4 \ - --hash=sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0 \ - --hash=sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad \ - --hash=sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa \ - --hash=sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f \ - --hash=sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1 \ - --hash=sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867 +orjson==3.12.0 \ + --hash=sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a \ + --hash=sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e \ + --hash=sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55 \ + --hash=sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c \ + --hash=sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed \ + --hash=sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b \ + --hash=sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54 \ + --hash=sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df \ + --hash=sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578 \ + --hash=sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c \ + --hash=sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83 \ + --hash=sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94 \ + --hash=sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710 \ + --hash=sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d \ + --hash=sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38 \ + --hash=sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e \ + --hash=sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7 \ + --hash=sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873 \ + --hash=sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f \ + --hash=sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328 \ + --hash=sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8 \ + --hash=sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868 \ + --hash=sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222 \ + --hash=sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc \ + --hash=sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e \ + --hash=sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806 \ + --hash=sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978 \ + --hash=sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc \ + --hash=sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647 \ + --hash=sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3 \ + --hash=sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13 \ + --hash=sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7 \ + --hash=sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900 \ + --hash=sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5 \ + --hash=sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d \ + --hash=sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a \ + --hash=sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10 \ + --hash=sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5 \ + --hash=sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e \ + --hash=sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92 \ + --hash=sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0 \ + --hash=sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03 \ + --hash=sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d \ + --hash=sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2 \ + --hash=sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998 \ + --hash=sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e \ + --hash=sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a \ + --hash=sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5 \ + --hash=sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92 \ + --hash=sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1 \ + --hash=sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e \ + --hash=sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8 \ + --hash=sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517 \ + --hash=sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1 \ + --hash=sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f \ + --hash=sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252 # via # langgraph-sdk # langsmith @@ -1100,190 +1448,145 @@ ormsgpack==1.12.2 \ --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd # via langgraph-checkpoint -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via # faiss-cpu # langchain-core # langsmith - # marshmallow -pandas==3.0.1 \ - --hash=sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8 \ - --hash=sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955 \ - --hash=sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7 \ - --hash=sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d \ - --hash=sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5 \ - --hash=sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee \ - --hash=sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791 \ - --hash=sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796 \ - --hash=sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff \ - --hash=sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1 \ - --hash=sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f \ - --hash=sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389 \ - --hash=sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8 \ - --hash=sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937 \ - --hash=sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d \ - --hash=sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1 \ - --hash=sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f \ - --hash=sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262 \ - --hash=sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4 \ - --hash=sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43 \ - --hash=sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749 \ - --hash=sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32 \ - --hash=sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9 \ - --hash=sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56 \ - --hash=sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76 \ - --hash=sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098 \ - --hash=sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e \ - --hash=sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7 \ - --hash=sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79 \ - --hash=sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221 \ - --hash=sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66 \ - --hash=sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988 \ - --hash=sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132 \ - --hash=sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447 \ - --hash=sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a \ - --hash=sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821 \ - --hash=sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25 \ - --hash=sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf \ - --hash=sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59 \ - --hash=sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324 \ - --hash=sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35 \ - --hash=sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b \ - --hash=sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249 \ - --hash=sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c \ - --hash=sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87 \ - --hash=sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06 \ - --hash=sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea \ - --hash=sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a - # via pyrag-kit -prompt-toolkit==3.0.52 \ - --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ - --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 + # pyinstaller + # pyinstaller-hooks-contrib + # pytest +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + # via mypy +pefile==2024.8.26 ; sys_platform == 'win32' \ + --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ + --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f + # via pyinstaller +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 # via # pyrag-kit # questionary -propcache==0.4.1 \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.27.1 \ - --hash=sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147 \ - --hash=sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc - # via - # google-ai-generativelanguage - # google-api-core -protobuf==5.29.6 \ - --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ - --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ - --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ - --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ - --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ - --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ - --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 - # via - # google-ai-generativelanguage - # google-api-core - # google-generativeai - # googleapis-common-protos - # grpcio-status - # proto-plus -pyasn1==0.6.3 \ - --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \ - --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ @@ -1293,12 +1596,12 @@ pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementatio --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.12.5 \ - --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ - --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d +pydantic==2.13.5 \ + --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 \ + --hash=sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08 # via # anthropic - # google-generativeai + # google-genai # langchain # langchain-classic # langchain-core @@ -1307,98 +1610,104 @@ pydantic==2.12.5 \ # openai # pydantic-settings # pyrag-kit -pydantic-core==2.41.5 \ - --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ - --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ - --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ - --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ - --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ - --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ - --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ - --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ - --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ - --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ - --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ - --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ - --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ - --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ - --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ - --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ - --hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \ - --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ - --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ - --hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \ - --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ - --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ - --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ - --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ - --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ - --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ - --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ - --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ - --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ - --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ - --hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \ - --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ - --hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \ - --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ - --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ - --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ - --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ - --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ - --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ - --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ - --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ - --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ - --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ - --hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \ - --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ - --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ - --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ - --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ - --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ - --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ - --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ - --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ - --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ - --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ - --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ - --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ - --hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \ - --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ - --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ - --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ - --hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \ - --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ - --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ - --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ - --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ - --hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \ - --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ - --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ - --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ - --hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \ - --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ - --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ - --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ - --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ - --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ - --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ - --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ - --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ - --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ - --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ - --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ - --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ - --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ - --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \ - --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 + # volcengine-python-sdk +pydantic-core==2.46.5 \ + --hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \ + --hash=sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821 \ + --hash=sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5 \ + --hash=sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c \ + --hash=sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184 \ + --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 \ + --hash=sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc \ + --hash=sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5 \ + --hash=sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0 \ + --hash=sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931 \ + --hash=sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e \ + --hash=sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25 \ + --hash=sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47 \ + --hash=sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038 \ + --hash=sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8 \ + --hash=sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b \ + --hash=sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a \ + --hash=sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e \ + --hash=sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074 \ + --hash=sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0 \ + --hash=sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433 \ + --hash=sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034 \ + --hash=sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21 \ + --hash=sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c \ + --hash=sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4 \ + --hash=sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c \ + --hash=sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c \ + --hash=sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061 \ + --hash=sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29 \ + --hash=sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3 \ + --hash=sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa \ + --hash=sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e \ + --hash=sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38 \ + --hash=sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f \ + --hash=sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b \ + --hash=sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8 \ + --hash=sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e \ + --hash=sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c \ + --hash=sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be \ + --hash=sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1 \ + --hash=sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b \ + --hash=sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f \ + --hash=sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761 \ + --hash=sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869 \ + --hash=sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111 \ + --hash=sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5 \ + --hash=sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7 \ + --hash=sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129 \ + --hash=sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c \ + --hash=sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec \ + --hash=sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689 \ + --hash=sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f \ + --hash=sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea \ + --hash=sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9 \ + --hash=sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519 \ + --hash=sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f \ + --hash=sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee \ + --hash=sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a \ + --hash=sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e \ + --hash=sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6 \ + --hash=sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2 \ + --hash=sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f \ + --hash=sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a \ + --hash=sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f \ + --hash=sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a \ + --hash=sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47 \ + --hash=sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda \ + --hash=sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0 \ + --hash=sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4 \ + --hash=sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d \ + --hash=sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3 \ + --hash=sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2 \ + --hash=sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355 \ + --hash=sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f \ + --hash=sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5 \ + --hash=sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2 \ + --hash=sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829 \ + --hash=sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266 \ + --hash=sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575 \ + --hash=sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290 \ + --hash=sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f \ + --hash=sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62 \ + --hash=sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f \ + --hash=sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a \ + --hash=sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7 \ + --hash=sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed \ + --hash=sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f \ + --hash=sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3 \ + --hash=sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff \ + --hash=sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084 \ + --hash=sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0 \ + --hash=sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9 # via pydantic -pydantic-settings==2.13.1 \ - --hash=sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025 \ - --hash=sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237 +pydantic-settings==2.15.0 \ + --hash=sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42 \ + --hash=sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117 # via # langchain-community # pyrag-kit @@ -1406,22 +1715,44 @@ pyfiglet==1.0.4 \ --hash=sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f \ --hash=sha256:db9c9940ed1bf3048deff534ed52ff2dafbbc2cd7610b17bb5eca1df6d4278ef # via pyrag-kit -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via rich -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc - # via httplib2 +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via + # pytest + # rich +pyinstaller==6.22.2 \ + --hash=sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1 \ + --hash=sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4 \ + --hash=sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4 \ + --hash=sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd \ + --hash=sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0 \ + --hash=sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096 \ + --hash=sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9 \ + --hash=sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7 \ + --hash=sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e \ + --hash=sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d \ + --hash=sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d \ + --hash=sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c +pyinstaller-hooks-contrib==2026.7 \ + --hash=sha256:24257a04c7a5a7a034cf28e39dcee20fbeeb9f043076729480f2e1b69904408a \ + --hash=sha256:5fbcaacb22c4f4aac869a127dce283f67a4b4cfcc37d496f2446603e6d68aefa + # via pyinstaller +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via pandas -python-dotenv==1.2.2 \ - --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ - --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 + # via volcengine-python-sdk +python-dotenv==1.2.3 \ + --hash=sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9 \ + --hash=sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35 # via pydantic-settings +pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ + --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ + --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 + # via pyinstaller pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ @@ -1472,6 +1803,7 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via + # bandit # langchain-classic # langchain-community # langchain-core @@ -1483,110 +1815,127 @@ rank-bm25==0.2.2 \ --hash=sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d \ --hash=sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae # via pyrag-kit -regex==2026.2.28 \ - --hash=sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1 \ - --hash=sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a \ - --hash=sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4 \ - --hash=sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d \ - --hash=sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a \ - --hash=sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911 \ - --hash=sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952 \ - --hash=sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b \ - --hash=sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97 \ - --hash=sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25 \ - --hash=sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8 \ - --hash=sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff \ - --hash=sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a \ - --hash=sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7 \ - --hash=sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0 \ - --hash=sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215 \ - --hash=sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43 \ - --hash=sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b \ - --hash=sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e \ - --hash=sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae \ - --hash=sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8 \ - --hash=sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11 \ - --hash=sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae \ - --hash=sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5 \ - --hash=sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64 \ - --hash=sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472 \ - --hash=sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18 \ - --hash=sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a \ - --hash=sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd \ - --hash=sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d \ - --hash=sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d \ - --hash=sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9 \ - --hash=sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96 \ - --hash=sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784 \ - --hash=sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b \ - --hash=sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff \ - --hash=sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc \ - --hash=sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf \ - --hash=sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098 \ - --hash=sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2 \ - --hash=sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05 \ - --hash=sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf \ - --hash=sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6 \ - --hash=sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768 \ - --hash=sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15 \ - --hash=sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881 \ - --hash=sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f \ - --hash=sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8 \ - --hash=sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c \ - --hash=sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d \ - --hash=sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341 \ - --hash=sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e \ - --hash=sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2 \ - --hash=sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550 \ - --hash=sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e \ - --hash=sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8 \ - --hash=sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59 \ - --hash=sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3 \ - --hash=sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117 \ - --hash=sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc \ - --hash=sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea \ - --hash=sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b \ - --hash=sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e \ - --hash=sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703 \ - --hash=sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2 \ - --hash=sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952 \ - --hash=sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944 \ - --hash=sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7 \ - --hash=sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b \ - --hash=sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033 \ - --hash=sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4 \ - --hash=sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8 \ - --hash=sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f \ - --hash=sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d \ - --hash=sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d \ - --hash=sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec \ - --hash=sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b \ - --hash=sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a \ - --hash=sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92 \ - --hash=sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9 \ - --hash=sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc \ - --hash=sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022 \ - --hash=sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6 \ - --hash=sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c \ - --hash=sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27 \ - --hash=sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b \ - --hash=sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc \ - --hash=sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1 \ - --hash=sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07 \ - --hash=sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c \ - --hash=sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a \ - --hash=sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33 \ - --hash=sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95 \ - --hash=sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081 \ - --hash=sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7 \ - --hash=sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb \ - --hash=sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61 +regex==2026.9.3 \ + --hash=sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab \ + --hash=sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6 \ + --hash=sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089 \ + --hash=sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc \ + --hash=sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae \ + --hash=sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27 \ + --hash=sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd \ + --hash=sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36 \ + --hash=sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280 \ + --hash=sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e \ + --hash=sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71 \ + --hash=sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687 \ + --hash=sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e \ + --hash=sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a \ + --hash=sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4 \ + --hash=sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35 \ + --hash=sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803 \ + --hash=sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae \ + --hash=sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16 \ + --hash=sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544 \ + --hash=sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211 \ + --hash=sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69 \ + --hash=sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce \ + --hash=sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b \ + --hash=sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420 \ + --hash=sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5 \ + --hash=sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596 \ + --hash=sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141 \ + --hash=sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4 \ + --hash=sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14 \ + --hash=sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846 \ + --hash=sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08 \ + --hash=sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23 \ + --hash=sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69 \ + --hash=sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa \ + --hash=sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd \ + --hash=sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595 \ + --hash=sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5 \ + --hash=sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b \ + --hash=sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f \ + --hash=sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77 \ + --hash=sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e \ + --hash=sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151 \ + --hash=sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17 \ + --hash=sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3 \ + --hash=sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253 \ + --hash=sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c \ + --hash=sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20 \ + --hash=sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7 \ + --hash=sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce \ + --hash=sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be \ + --hash=sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69 \ + --hash=sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90 \ + --hash=sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b \ + --hash=sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950 \ + --hash=sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec \ + --hash=sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65 \ + --hash=sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124 \ + --hash=sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970 \ + --hash=sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c \ + --hash=sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944 \ + --hash=sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753 \ + --hash=sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3 \ + --hash=sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7 \ + --hash=sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc \ + --hash=sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328 \ + --hash=sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275 \ + --hash=sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16 \ + --hash=sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01 \ + --hash=sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380 \ + --hash=sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886 \ + --hash=sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef \ + --hash=sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f \ + --hash=sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8 \ + --hash=sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c \ + --hash=sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db \ + --hash=sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c \ + --hash=sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177 \ + --hash=sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5 \ + --hash=sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9 \ + --hash=sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b \ + --hash=sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e \ + --hash=sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361 \ + --hash=sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f \ + --hash=sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de \ + --hash=sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc \ + --hash=sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16 \ + --hash=sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092 \ + --hash=sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8 \ + --hash=sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57 \ + --hash=sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96 \ + --hash=sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76 \ + --hash=sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a \ + --hash=sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec \ + --hash=sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e \ + --hash=sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28 \ + --hash=sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d \ + --hash=sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136 \ + --hash=sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e \ + --hash=sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f \ + --hash=sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181 \ + --hash=sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32 \ + --hash=sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9 \ + --hash=sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9 \ + --hash=sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20 \ + --hash=sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2 \ + --hash=sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff \ + --hash=sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7 \ + --hash=sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88 \ + --hash=sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec \ + --hash=sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f \ + --hash=sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b \ + --hash=sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb # via tiktoken -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # google-api-core + # google-auth + # google-genai # langchain-classic # langchain-community # langsmith @@ -1597,508 +1946,631 @@ requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via langsmith -rich==14.3.3 \ - --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ - --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b - # via pyrag-kit -scikit-learn==1.8.0 \ - --hash=sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2 \ - --hash=sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a \ - --hash=sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da \ - --hash=sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9 \ - --hash=sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961 \ - --hash=sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6 \ - --hash=sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271 \ - --hash=sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809 \ - --hash=sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242 \ - --hash=sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4 \ - --hash=sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7 \ - --hash=sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76 \ - --hash=sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6 \ - --hash=sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b \ - --hash=sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e \ - --hash=sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7 \ - --hash=sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e \ - --hash=sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57 \ - --hash=sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735 \ - --hash=sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb \ - --hash=sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb \ - --hash=sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e \ - --hash=sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd \ - --hash=sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a \ - --hash=sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9 \ - --hash=sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1 \ - --hash=sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde \ - --hash=sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3 \ - --hash=sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f \ - --hash=sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b \ - --hash=sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3 \ - --hash=sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e \ - --hash=sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702 \ - --hash=sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c \ - --hash=sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1 \ - --hash=sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4 \ - --hash=sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd - # via pyrag-kit -scipy==1.17.1 \ - --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ - --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ - --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ - --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ - --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ - --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ - --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ - --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ - --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ - --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ - --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ - --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ - --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ - --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ - --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ - --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ - --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ - --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ - --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ - --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ - --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ - --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ - --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ - --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ - --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ - --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ - --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ - --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ - --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ - --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ - --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ - --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ - --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ - --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ - --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ - --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ - --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ - --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ - --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ - --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ - --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ - --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ - --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ - --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ - --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ - --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ - --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ - --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ - --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ - --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ - --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ - --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ - --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ - --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ - --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ - --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ - --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ - --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ - --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ - --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ - --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 - # via scikit-learn +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via + # bandit + # pyrag-kit +ruff==0.16.6 \ + --hash=sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70 \ + --hash=sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25 \ + --hash=sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39 \ + --hash=sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3 \ + --hash=sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88 \ + --hash=sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d \ + --hash=sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953 \ + --hash=sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8 \ + --hash=sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e \ + --hash=sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5 \ + --hash=sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876 \ + --hash=sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c \ + --hash=sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718 \ + --hash=sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4 \ + --hash=sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757 \ + --hash=sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050 \ + --hash=sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1 \ + --hash=sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32 +setuptools==84.0.0 \ + --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ + --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 + # via pyinstaller six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via python-dateutil + # via + # python-dateutil + # volcengine-python-sdk sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via # anthropic + # google-genai + # langsmith # openai -sqlalchemy==2.0.48 \ - --hash=sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e \ - --hash=sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6 \ - --hash=sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0 \ - --hash=sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc \ - --hash=sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b \ - --hash=sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f \ - --hash=sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0 \ - --hash=sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894 \ - --hash=sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b \ - --hash=sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8 \ - --hash=sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131 \ - --hash=sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b \ - --hash=sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f \ - --hash=sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb \ - --hash=sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9 \ - --hash=sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c \ - --hash=sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241 \ - --hash=sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658 \ - --hash=sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7 \ - --hash=sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a \ - --hash=sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae \ - --hash=sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7 \ - --hash=sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3 \ - --hash=sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0 \ - --hash=sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2 \ - --hash=sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d \ - --hash=sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99 \ - --hash=sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485 \ - --hash=sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617 \ - --hash=sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096 \ - --hash=sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed \ - --hash=sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f \ - --hash=sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2 \ - --hash=sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933 \ - --hash=sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b \ - --hash=sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd \ - --hash=sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79 \ - --hash=sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4 \ - --hash=sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571 \ - --hash=sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c \ - --hash=sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121 \ - --hash=sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb +sqlalchemy==2.0.52 \ + --hash=sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee \ + --hash=sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca \ + --hash=sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43 \ + --hash=sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee \ + --hash=sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89 \ + --hash=sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9 \ + --hash=sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736 \ + --hash=sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd \ + --hash=sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8 \ + --hash=sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97 \ + --hash=sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43 \ + --hash=sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c \ + --hash=sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608 \ + --hash=sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437 \ + --hash=sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b \ + --hash=sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1 \ + --hash=sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1 \ + --hash=sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751 \ + --hash=sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d \ + --hash=sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057 \ + --hash=sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2 \ + --hash=sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e \ + --hash=sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15 \ + --hash=sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727 \ + --hash=sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582 \ + --hash=sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d \ + --hash=sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72 \ + --hash=sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e \ + --hash=sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c \ + --hash=sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf \ + --hash=sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc # via # langchain-classic # langchain-community +stevedore==5.9.1 \ + --hash=sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf \ + --hash=sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf + # via bandit tenacity==9.1.4 \ --hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \ --hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a # via + # google-genai # langchain-community # langchain-core -threadpoolctl==3.6.0 \ - --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ - --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e - # via scikit-learn -tiktoken==0.12.0 \ - --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ - --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ - --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ - --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ - --hash=sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff \ - --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ - --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ - --hash=sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3 \ - --hash=sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def \ - --hash=sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded \ - --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ - --hash=sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd \ - --hash=sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a \ - --hash=sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0 \ - --hash=sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0 \ - --hash=sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b \ - --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ - --hash=sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb \ - --hash=sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3 \ - --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ - --hash=sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b \ - --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ - --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ - --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ - --hash=sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967 \ - --hash=sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646 \ - --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ - --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ - --hash=sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697 \ - --hash=sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8 \ - --hash=sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa \ - --hash=sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365 \ - --hash=sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e \ - --hash=sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830 \ - --hash=sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16 \ - --hash=sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88 \ - --hash=sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f \ - --hash=sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63 \ - --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ - --hash=sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc \ - --hash=sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71 \ - --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 \ - --hash=sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd + # pyrag-kit +tiktoken==0.14.0 \ + --hash=sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3 \ + --hash=sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890 \ + --hash=sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78 \ + --hash=sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3 \ + --hash=sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232 \ + --hash=sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e \ + --hash=sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695 \ + --hash=sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea \ + --hash=sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f \ + --hash=sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06 \ + --hash=sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874 \ + --hash=sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef \ + --hash=sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d \ + --hash=sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771 \ + --hash=sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae \ + --hash=sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a \ + --hash=sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010 \ + --hash=sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f \ + --hash=sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6 \ + --hash=sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632 \ + --hash=sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9 \ + --hash=sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4 \ + --hash=sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438 \ + --hash=sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871 \ + --hash=sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1 \ + --hash=sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d \ + --hash=sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0 \ + --hash=sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6 \ + --hash=sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa \ + --hash=sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89 \ + --hash=sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36 \ + --hash=sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1 \ + --hash=sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c \ + --hash=sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482 \ + --hash=sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79 \ + --hash=sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da \ + --hash=sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58 \ + --hash=sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94 \ + --hash=sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948 \ + --hash=sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5 \ + --hash=sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4 \ + --hash=sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450 \ + --hash=sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037 \ + --hash=sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42 \ + --hash=sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49 \ + --hash=sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f \ + --hash=sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098 \ + --hash=sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b \ + --hash=sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513 \ + --hash=sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e # via pyrag-kit -tqdm==4.67.3 \ - --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ - --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf +truststore==0.10.4 ; sys_platform != 'emscripten' \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 # via - # google-generativeai - # openai -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # httpcore2 + # httpx2 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anthropic # anyio - # google-generativeai - # grpcio + # google-genai + # httpx2 # langchain-core + # langchain-protocol + # langsmith + # mypy # openai # pydantic # pydantic-core # sqlalchemy - # typing-inspect # typing-inspection -typing-inspect==0.9.0 \ - --hash=sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f \ - --hash=sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78 - # via dataclasses-json -typing-inspection==0.4.2 \ - --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ - --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 # via # pydantic # pydantic-settings -tzdata==2025.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' \ - --hash=sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 \ - --hash=sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7 - # via pandas -uritemplate==4.2.0 \ - --hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \ - --hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 - # via google-api-python-client -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests -uuid-utils==0.14.1 \ - --hash=sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b \ - --hash=sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3 \ - --hash=sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf \ - --hash=sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c \ - --hash=sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531 \ - --hash=sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2 \ - --hash=sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62 \ - --hash=sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae \ - --hash=sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0 \ - --hash=sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69 \ - --hash=sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a \ - --hash=sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8 \ - --hash=sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523 \ - --hash=sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1 \ - --hash=sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96 \ - --hash=sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43 \ - --hash=sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01 \ - --hash=sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297 \ - --hash=sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741 \ - --hash=sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862 \ - --hash=sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533 \ - --hash=sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via + # requests + # volcengine-python-sdk +uuid-utils==0.17.0 \ + --hash=sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5 \ + --hash=sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803 \ + --hash=sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869 \ + --hash=sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d \ + --hash=sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2 \ + --hash=sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6 \ + --hash=sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968 \ + --hash=sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7 \ + --hash=sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70 \ + --hash=sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9 \ + --hash=sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099 \ + --hash=sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3 \ + --hash=sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc \ + --hash=sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc \ + --hash=sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91 \ + --hash=sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2 \ + --hash=sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310 \ + --hash=sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343 \ + --hash=sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb \ + --hash=sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa \ + --hash=sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1 \ + --hash=sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9 \ + --hash=sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f \ + --hash=sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750 \ + --hash=sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a \ + --hash=sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988 \ + --hash=sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0 \ + --hash=sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64 \ + --hash=sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0 \ + --hash=sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3 \ + --hash=sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d \ + --hash=sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c \ + --hash=sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7 \ + --hash=sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6 \ + --hash=sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131 \ + --hash=sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1 \ + --hash=sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a \ + --hash=sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098 \ + --hash=sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46 \ + --hash=sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd \ + --hash=sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e \ + --hash=sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a \ + --hash=sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68 \ + --hash=sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0 \ + --hash=sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb \ + --hash=sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73 \ + --hash=sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf \ + --hash=sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89 \ + --hash=sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391 \ + --hash=sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a \ + --hash=sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc \ + --hash=sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4 \ + --hash=sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287 \ + --hash=sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2 \ + --hash=sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b \ + --hash=sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912 \ + --hash=sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1 \ + --hash=sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb \ + --hash=sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff \ + --hash=sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330 \ + --hash=sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2 \ + --hash=sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c \ + --hash=sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae \ + --hash=sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5 \ + --hash=sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479 \ + --hash=sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f \ + --hash=sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13 \ + --hash=sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63 \ + --hash=sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354 \ + --hash=sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652 \ + --hash=sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354 \ + --hash=sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec \ + --hash=sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a \ + --hash=sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206 \ + --hash=sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab \ + --hash=sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637 \ + --hash=sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94 \ + --hash=sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd \ + --hash=sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0 \ + --hash=sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371 \ + --hash=sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0 # via # langchain-core # langsmith -wcwidth==0.6.0 \ - --hash=sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad \ - --hash=sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159 +volcengine-python-sdk==5.0.48 \ + --hash=sha256:05c15f9138e016e479cdf29971d1e5fedbb9e8f4378930c68e2a9622d087995c \ + --hash=sha256:bb78995e35c34ad7a24e535826a1cb197e1209783c8a7dc11b001a09cb4bc48c + # via pyrag-kit +wcwidth==0.8.3 \ + --hash=sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb \ + --hash=sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4 # via prompt-toolkit -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 + # via + # google-genai + # langgraph-sdk + # langsmith +xxhash==4.0.1 \ + --hash=sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b \ + --hash=sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c \ + --hash=sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3 \ + --hash=sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa \ + --hash=sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a \ + --hash=sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37 \ + --hash=sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1 \ + --hash=sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535 \ + --hash=sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24 \ + --hash=sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433 \ + --hash=sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684 \ + --hash=sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91 \ + --hash=sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf \ + --hash=sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706 \ + --hash=sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea \ + --hash=sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2 \ + --hash=sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428 \ + --hash=sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256 \ + --hash=sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee \ + --hash=sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60 \ + --hash=sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d \ + --hash=sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f \ + --hash=sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc \ + --hash=sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34 \ + --hash=sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2 \ + --hash=sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399 \ + --hash=sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128 \ + --hash=sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df \ + --hash=sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a \ + --hash=sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478 \ + --hash=sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1 \ + --hash=sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794 \ + --hash=sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619 \ + --hash=sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6 \ + --hash=sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f \ + --hash=sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c \ + --hash=sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f \ + --hash=sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d \ + --hash=sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0 \ + --hash=sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb \ + --hash=sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68 \ + --hash=sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a \ + --hash=sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1 \ + --hash=sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b \ + --hash=sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec \ + --hash=sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d \ + --hash=sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f \ + --hash=sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc \ + --hash=sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6 \ + --hash=sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0 \ + --hash=sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c \ + --hash=sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b \ + --hash=sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51 \ + --hash=sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c \ + --hash=sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e \ + --hash=sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9 \ + --hash=sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56 \ + --hash=sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc \ + --hash=sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848 \ + --hash=sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794 \ + --hash=sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e \ + --hash=sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330 \ + --hash=sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a \ + --hash=sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a \ + --hash=sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f \ + --hash=sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0 \ + --hash=sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e \ + --hash=sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd \ + --hash=sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57 \ + --hash=sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8 \ + --hash=sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3 \ + --hash=sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626 \ + --hash=sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88 \ + --hash=sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f \ + --hash=sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa \ + --hash=sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513 \ + --hash=sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd \ + --hash=sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c \ + --hash=sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7 \ + --hash=sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a \ + --hash=sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670 \ + --hash=sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00 \ + --hash=sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c \ + --hash=sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468 \ + --hash=sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d \ + --hash=sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3 \ + --hash=sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a \ + --hash=sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e \ + --hash=sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9 \ + --hash=sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54 \ + --hash=sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377 \ + --hash=sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6 \ + --hash=sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f \ + --hash=sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375 \ + --hash=sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9 \ + --hash=sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824 \ + --hash=sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349 \ + --hash=sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6 \ + --hash=sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089 \ + --hash=sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d \ + --hash=sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53 \ + --hash=sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf \ + --hash=sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb \ + --hash=sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea \ + --hash=sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0 \ + --hash=sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e \ + --hash=sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914 \ + --hash=sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269 \ + --hash=sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503 \ + --hash=sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0 \ + --hash=sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360 \ + --hash=sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5 \ + --hash=sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621 \ + --hash=sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df \ + --hash=sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191 \ + --hash=sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a \ + --hash=sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286 \ + --hash=sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f \ + --hash=sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd \ + --hash=sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee \ + --hash=sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0 \ + --hash=sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a \ + --hash=sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1 \ + --hash=sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579 \ + --hash=sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592 \ + --hash=sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c \ + --hash=sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d \ + --hash=sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6 \ + --hash=sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736 \ + --hash=sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59 \ + --hash=sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d \ + --hash=sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788 \ + --hash=sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276 \ + --hash=sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056 \ + --hash=sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa \ + --hash=sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7 \ + --hash=sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873 \ + --hash=sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e \ + --hash=sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2 \ + --hash=sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd \ + --hash=sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f \ + --hash=sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8 \ + --hash=sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2 \ + --hash=sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79 \ + --hash=sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd \ + --hash=sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5 \ + --hash=sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329 \ + --hash=sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9 \ + --hash=sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c \ + --hash=sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b \ + --hash=sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e \ + --hash=sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875 \ + --hash=sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d \ + --hash=sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b \ + --hash=sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32 \ + --hash=sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1 \ + --hash=sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d \ + --hash=sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451 \ + --hash=sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a \ + --hash=sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925 \ + --hash=sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58 \ + --hash=sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a \ + --hash=sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80 \ + --hash=sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d # via # langgraph # langsmith -yarl==1.23.0 \ - --hash=sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc \ - --hash=sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4 \ - --hash=sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85 \ - --hash=sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222 \ - --hash=sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de \ - --hash=sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 \ - --hash=sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e \ - --hash=sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2 \ - --hash=sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e \ - --hash=sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860 \ - --hash=sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957 \ - --hash=sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760 \ - --hash=sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52 \ - --hash=sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788 \ - --hash=sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912 \ - --hash=sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 \ - --hash=sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 \ - --hash=sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412 \ - --hash=sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41 \ - --hash=sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 \ - --hash=sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd \ - --hash=sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a \ - --hash=sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4 \ - --hash=sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34 \ - --hash=sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069 \ - --hash=sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25 \ - --hash=sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2 \ - --hash=sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb \ - --hash=sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f \ - --hash=sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5 \ - --hash=sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8 \ - --hash=sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c \ - --hash=sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512 \ - --hash=sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6 \ - --hash=sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5 \ - --hash=sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9 \ - --hash=sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072 \ - --hash=sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5 \ - --hash=sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277 \ - --hash=sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a \ - --hash=sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae \ - --hash=sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2 \ - --hash=sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4 \ - --hash=sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70 \ - --hash=sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723 \ - --hash=sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c \ - --hash=sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9 \ - --hash=sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5 \ - --hash=sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e \ - --hash=sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c \ - --hash=sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4 \ - --hash=sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2 \ - --hash=sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b \ - --hash=sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7 \ - --hash=sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2 \ - --hash=sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474 \ - --hash=sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7 \ - --hash=sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123 \ - --hash=sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595 \ - --hash=sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe \ - --hash=sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea \ - --hash=sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 \ - --hash=sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679 \ - --hash=sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8 \ - --hash=sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83 \ - --hash=sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6 \ - --hash=sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f \ - --hash=sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94 \ - --hash=sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 \ - --hash=sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120 \ - --hash=sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039 \ - --hash=sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1 \ - --hash=sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05 \ - --hash=sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144 \ - --hash=sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa \ - --hash=sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a \ - --hash=sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99 \ - --hash=sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928 \ - --hash=sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d \ - --hash=sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3 \ - --hash=sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434 \ - --hash=sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86 \ - --hash=sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46 \ - --hash=sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319 \ - --hash=sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67 \ - --hash=sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c \ - --hash=sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169 \ - --hash=sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c \ - --hash=sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59 \ - --hash=sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4 \ - --hash=sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a \ - --hash=sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb \ - --hash=sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f \ - --hash=sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 \ - --hash=sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090 \ - --hash=sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4 \ - --hash=sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b \ - --hash=sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543 \ - --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 \ - --hash=sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5 \ - --hash=sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b \ - --hash=sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b \ - --hash=sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6 \ - --hash=sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735 \ - --hash=sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e \ - --hash=sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28 \ - --hash=sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3 \ - --hash=sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401 \ - --hash=sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6 \ - --hash=sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ diff --git a/uv.lock b/uv.lock index 3f49626..971d95b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,29 +2,25 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", ] [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -33,95 +29,111 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -148,43 +160,106 @@ wheels = [ [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "anthropic" -version = "0.86.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, { name = "docstring-parser" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/6d/793f5cfe2cd444c43b4eeb4cb7c3cc55ebcb38929fdfe81aa1f2fced7326/anthropic-1.4.0.tar.gz", hash = "sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c", size = 1150831, upload-time = "2026-09-04T22:20:31.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/34a88f0e1e854022a352d67f72ca9baa8b952d4541315088411ba2bfbc2a/anthropic-1.4.0-py3-none-any.whl", hash = "sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c", size = 1300390, upload-time = "2026-09-04T22:20:33.078Z" }, ] [[package]] name = "anyio" -version = "4.12.1" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.tar.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 951293, upload-time = "2026-09-02T15:50:45.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/76/497f19d9bdb3899a1efd82e2957f455d0c6e0cb9ebbc254735acb1f74235/ast_serialize-0.9.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ae1c46eb97865823f9843c4b80145e011874923e1a4a44b45738a5309d83e9f5", size = 889442, upload-time = "2026-09-02T15:49:21.144Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c5/9fb64b7106c5534739322c74be7b743c4f2e3b5fd05d5b8e677f05c54d5f/ast_serialize-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af082cb7e6c4fa3a428aa616c13d709a944076eba84a73184de15621cc1a915d", size = 1226721, upload-time = "2026-09-02T15:49:22.612Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/b550fc81aa0133808410783c6d9a1b925e31610d226e836e21337850af55/ast_serialize-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e9f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7", size = 1207369, upload-time = "2026-09-02T15:49:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/ed9e66deb7da63e44d0c0fd3a8feef698882ed56ea521a29494d4616eb46/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2b4c3da4", size = 1273073, upload-time = "2026-09-02T15:49:25.336Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/cd337551d7a68c982425bbf91f183943d7ccc62394002c74807a7f0e60db/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b559cffac5a71a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e", size = 1279045, upload-time = "2026-09-02T15:49:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/98dc41eb4122d5da83241e805739838ed59e1e1b9006cbed89ded635a17f/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f", size = 1539300, upload-time = "2026-09-02T15:49:28.213Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/d94cede4b3f2a4e329d8ca92218f0846cfcc9257be91c5bf1168671f4ab5/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24de2bc930b7e1ca86641136b9875a1e5f80f52b484deddc67893eeaf9077bd9", size = 1291957, upload-time = "2026-09-02T15:49:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/8e/85/8ac18d754225cf13392786b23d4ba84273ceee562699362f22e61942ce64/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac7b4cdf89ca8318aae157d824017596784982851c8a89a621973f261574696", size = 1291779, upload-time = "2026-09-02T15:49:31.199Z" }, + { url = "https://files.pythonhosted.org/packages/62/ed/cc757fec9e96e29f19a6f818e05147e4f2949356258a3243412756f22a2e/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:95dcb93f30258dcf09d9b6a302dac9323b70e9df8c18ee0bf4ea1fa7cc5f1875", size = 1299730, upload-time = "2026-09-02T15:49:32.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8c/a575ae0ae954f21a187b4c1d8cec28d81a009693bd13a1388eec72d9b55a/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed5824b4b2fa37ad93fa2db23d8243a3d315b3e2d7ca70f99b2727d8788af05", size = 1344671, upload-time = "2026-09-02T15:49:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/0b2b15c0ae5f9a95f433016d1a59a3227eebbb378654d6354206c8ac8e8d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3c69f2ce565c786bd3853ce42495e8a63610516f79651c7cb4f2cd0ddfaee52", size = 1448527, upload-time = "2026-09-02T15:49:35.68Z" }, + { url = "https://files.pythonhosted.org/packages/18/be/ee89cb6a5d3427946532f0611b514befdd69564803e9a9f9ce712f9d2654/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8c824d822a2ac54ec4228b88c0d170ab0024cf285eeee57b9d4f994003fb553", size = 1554045, upload-time = "2026-09-02T15:49:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4d/eaada807f98a2f0d370fec4b46c84f0e551a62911e751a76ecb32bef4dde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c55010ea0fafc6bc8231809d328bda781bfe41a01c43522be5eb3713fd855cda", size = 1547578, upload-time = "2026-09-02T15:49:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/046c531f4af4e1bc3314077a84b3099f38b45e2d969faa24acedb3066d92/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:322282ac5337e5e776bc416f2b5201e680fa4dacf92ea739bd35e46a28a66c41", size = 1671896, upload-time = "2026-09-02T15:49:40.273Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/8d32aa0cf4e3e566399b2079a4c47f8ad3c62155f6ee1fe63631b6d3fdde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ded5ed06ec469407d6cd571ace7a7a25809cc388e4bac1dd35c7747469fc7fdf", size = 1472895, upload-time = "2026-09-02T15:49:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/8204c7e3d8abd4b0c7a56a8d5cce05fcacfd6a5d63ffbd812b8b94040d6d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3ee40752ddb4fb5c6a67161d13f3ce3df7987dcb9272260542a86b0be1519ae2", size = 1492731, upload-time = "2026-09-02T15:49:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/f3/72/ff5c44c19409686798feeb1fbe209f2be78b4b64948d5aa2ddeec8901591/ast_serialize-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:d9c635eacfc02b91da6796d3b5ff9086e511b8a29b19f9b3f4f978b8d170f838", size = 1112847, upload-time = "2026-09-02T15:49:45.181Z" }, + { url = "https://files.pythonhosted.org/packages/20/75/fa5be1a94d189adafadf9c5f07fffd66af7a8061c4cff92f75285ef79d10/ast_serialize-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:62a96e327e2a178d6c295b10422e95c992a9286f0ec1b2bc7cd5b4873252a38c", size = 1146846, upload-time = "2026-09-02T15:49:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ef/b2bfb331b6e379d435543f6d3be0b590e7a2f441d31ccc17d75f2d2d7cb8/ast_serialize-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:41da4332492222d56345d5e436eed4fbec76caadee959f6ffa3cd2fc1bd51895", size = 1119605, upload-time = "2026-09-02T15:49:48.14Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f9/a4af1bf8b35927814c09d90c3965dbfaa75c489ba34372bffafbc2209f40/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48", size = 1226344, upload-time = "2026-09-02T15:49:49.79Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/d3a95823a803c21f5c9df595a0bb93aada22e7aa22bf875fe00d89422d7f/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063", size = 1207384, upload-time = "2026-09-02T15:49:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/f0/97/6e7f46c8455b738609c29d1b7655307a168c4b40ce4c7a2c678c8ed9cf2e/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb", size = 1273139, upload-time = "2026-09-02T15:49:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6e/25b70733f061766865cb04d913dc5332037c595796b871d52ab5b569abb8/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0", size = 1278242, upload-time = "2026-09-02T15:49:54.236Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f0/b7820399d9c5a0b7f07c239b6da93d2e21a1b3785137fa00e16528e414b3/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb", size = 1541009, upload-time = "2026-09-02T15:49:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/08/56/5146f1d2a77516e697f6f42825df79137e43560675cb4605c467775f8b4a/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b", size = 1290898, upload-time = "2026-09-02T15:49:57.502Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/87cd16228796d703de795a369b90b0f57f0f017f90f55c4c5876e3513a03/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25", size = 1291742, upload-time = "2026-09-02T15:49:58.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/eb/13465c297268c5170b2bb746d75f37a8fad44a94a89b593071affc1071d0/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b", size = 1300180, upload-time = "2026-09-02T15:50:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a7/10b84c4274b2507b0ed9cc1654058ad64bcedb6ac574753d6a461bc6e204/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167", size = 1345857, upload-time = "2026-09-02T15:50:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/c7/dc/2702182c9773a15de9aabfaf66da7cb87548a56a6bac24f0c9176a4a13c3/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa", size = 1448544, upload-time = "2026-09-02T15:50:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/b5/eeef2124c9563b9861707ef4db91f153f3bb37b3e0cca9543bb88a4e9e53/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c", size = 1551572, upload-time = "2026-09-02T15:50:04.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8c/81d18349f1dffdcfeb80671bd737e342c86348e183692d9d0d8f573d1385/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321", size = 1548118, upload-time = "2026-09-02T15:50:06.522Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1f/339131b60d1b0df13d9f3470cfac70f858b5649188ea01b2e7b39caeb720/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2", size = 1674707, upload-time = "2026-09-02T15:50:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/18/0a/ca77596fa229d88f96eca180d45dbe8efa11306f8b2b4f4ee301b3fe465f/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c", size = 1473618, upload-time = "2026-09-02T15:50:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/88/5c/6aebeb54dd226b480014ff4488e150aa23b1de3204e2bf3f87de27e6542a/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09", size = 1492025, upload-time = "2026-09-02T15:50:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/c355d470a230f778311c28b80f6d934a497d094139f07230433eea18651b/ast_serialize-0.9.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf", size = 1113010, upload-time = "2026-09-02T15:50:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0d/66609ace58564727b68731293cc986c2ea1d5e6ef40e96571e7fb515f0af/ast_serialize-0.9.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10", size = 1146404, upload-time = "2026-09-02T15:50:13.761Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/da28e1c85f05fb9976f247d2a3aefce68866cb2939abcbdbddd9a5e3b835/ast_serialize-0.9.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3", size = 1118328, upload-time = "2026-09-02T15:50:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/487a158a99e4564244e000ed18475255dfea53fd34a84d8ca73633710500/ast_serialize-0.9.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5fc57f17fb4ce49b4eeccfcd2670e4a55659bd740bb8e8aedbe511ccab8b5f03", size = 889484, upload-time = "2026-09-02T15:50:16.643Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/175b0a64d6c96bc1b96598c6474ce8d1ef34e0b774bcf7183f4ce696fb10/ast_serialize-0.9.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d", size = 1232635, upload-time = "2026-09-02T15:50:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/28/0c/d51d8463aca43aaa833fdf1f25134d6cc1b483764896decca61306ad1f6e/ast_serialize-0.9.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7", size = 1219313, upload-time = "2026-09-02T15:50:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/ef/19/c88bdc64f86095a9d6ab325ae422b2a5e1395cd63cd8aa539003d4d4ae1d/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008", size = 1279981, upload-time = "2026-09-02T15:50:20.811Z" }, + { url = "https://files.pythonhosted.org/packages/86/58/a492075826df1753896dc8e8f6ababae4016d8883b670ee3a1c34788b154/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd", size = 1286319, upload-time = "2026-09-02T15:50:22.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/79/3f6754eaa42fd2a6c36aac066890870cd44cbe0e25f75a67b1b99a2f4d82/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82", size = 1551547, upload-time = "2026-09-02T15:50:23.528Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/8cfb7caadfaf28febaa6b61d31d778262f87f9366eda4dd9bd07ac940b75/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d", size = 1302468, upload-time = "2026-09-02T15:50:24.99Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e2/750a0b136bb02ff8e4a17d65a3a78cd478ee50724704df8215797a226ba3/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295", size = 1300563, upload-time = "2026-09-02T15:50:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/17/4c0aa852ff1e4f2d6723e8ce827136c1e1febf2845d7941ccc45426778de/ast_serialize-0.9.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24", size = 1308999, upload-time = "2026-09-02T15:50:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1b/6e73d0a29aedb0db30cc68f2557acaac06cd24c9783ccb90f84f89e4ce87/ast_serialize-0.9.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b", size = 1358191, upload-time = "2026-09-02T15:50:29.237Z" }, + { url = "https://files.pythonhosted.org/packages/dc/38/2cf5d552de99e0e9804a16fea73e54d0a7382498adddf57c0f6dc09cbc70/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47", size = 1458944, upload-time = "2026-09-02T15:50:30.77Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0b/5ef87adf955b6a027f616eb7b55f55a154c35ba600e9dd2d06ad2d30e5c2/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7", size = 1563421, upload-time = "2026-09-02T15:50:32.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/dd/9ced05a17feeb0f83e84010d80f5a1b7b7aa19e75f0376f4d3780803654c/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b", size = 1558536, upload-time = "2026-09-02T15:50:33.861Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8b/8ad486e44fc7081a2471055befc433dddc2e51c3a88dff141b3026f64602/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec", size = 1682749, upload-time = "2026-09-02T15:50:35.257Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4c/7c282aba9cfb0b92d79fac45c04e4557d9a7f08d872e5a43577a50867e30/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e", size = 1482441, upload-time = "2026-09-02T15:50:37.319Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/e45914e8cad81b660915f3784d255460a6384183b76bfc2089fdd79ec7df/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3", size = 1499042, upload-time = "2026-09-02T15:50:38.76Z" }, + { url = "https://files.pythonhosted.org/packages/99/09/6988921dec19c810beef53539fec2e90ae551cd93853b3303a99fe45f772/ast_serialize-0.9.0-cp39-abi3-win32.whl", hash = "sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550", size = 1116391, upload-time = "2026-09-02T15:50:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/fd/eb/839598a22a1f9af56d39e188451cad93dbcb0ce6539a45ac18fb8bf123fa/ast_serialize-0.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099", size = 1156055, upload-time = "2026-09-02T15:50:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" }, ] [[package]] @@ -196,172 +271,273 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, - { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, - { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, - { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, - { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, - { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, - { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -375,74 +551,58 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, -] - -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "typing-inspect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] [[package]] @@ -456,11 +616,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -474,27 +634,24 @@ wheels = [ [[package]] name = "faiss-cpu" -version = "1.13.2" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c9/671f66f6b31ec48e5825d36435f0cb91189fa8bb6b50724029dbff4ca83c/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1", size = 3452776, upload-time = "2025-12-24T10:27:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4a/97150aa1582fb9c2bca95bd8fc37f27d3b470acec6f0a6833844b21e4b40/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317", size = 7896434, upload-time = "2025-12-24T10:27:03.592Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d0/0940575f059591ca31b63a881058adb16a387020af1709dcb7669460115c/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1", size = 11485825, upload-time = "2025-12-24T10:27:05.681Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e1/a5acac02aa593809f0123539afe7b4aff61d1db149e7093239888c9053e1/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7", size = 23845772, upload-time = "2025-12-24T10:27:08.194Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7b/49dcaf354834ec457e85ca769d50bc9b5f3003fab7c94a9dcf08cf742793/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f", size = 13477567, upload-time = "2025-12-24T10:27:10.822Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6b/12bb4037921c38bb2c0b4cfc213ca7e04bbbebbfea89b0b5746248ce446e/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f", size = 25102239, upload-time = "2025-12-24T10:27:13.476Z" }, - { url = "https://files.pythonhosted.org/packages/14/6d/40439a05e4e60a0e889aa68b08ec70f5c8e32901f75f2be25c593a2e050e/faiss_cpu-1.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7c5944d7807d58fe7244b6aba06be710ee7ed99343365ed92699349efe979f51", size = 18879906, upload-time = "2025-12-24T10:27:19.041Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f9/b97eadbdd9e00f945d1566c7101382344f504596bfb19219465b0fc61e6e/faiss_cpu-1.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:19508a1badfb36e456c1c8664eeb948349f604db5c7545f277a0126b4a84b080", size = 8548280, upload-time = "2025-12-24T10:27:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/87/ff/35ed875423200c17bdd594ce921abfc1812ddd21e09355290b9a94e170ab/faiss_cpu-1.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:b82c01d30430dd7b1fa442001b9099735d1a82f6bb72033acdc9206d5ac66a64", size = 18890300, upload-time = "2025-12-24T10:27:24.194Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/bbdf5deaf6feb34b46b469c0a0acd40216c3d3c6ecf5aeb71d56b8a650e3/faiss_cpu-1.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2c4f696ae76e7c97cbc12311db83aaf1e7f4f7be06a3ffea7e5b0e8ec1fd805b", size = 8553157, upload-time = "2025-12-24T10:27:26.38Z" }, - { url = "https://files.pythonhosted.org/packages/60/4b/903d85bf3a8264d49964ec799e45c7ffc91098606b8bc9ef2c904c1a56cb/faiss_cpu-1.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:cb4b5ee184816a4b099162ac93c0d7f0033d81a88e7c1291d0a9cc41ec348984", size = 18891330, upload-time = "2025-12-24T10:27:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/b2/52/5d10642da628f63544aab27e48416be4a7ea25c6b81d8bd65016d8538b00/faiss_cpu-1.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:1243967eeb2298791ff7f3683a4abd2100d7e6ec7542ca05c3b75d47a7f621e5", size = 8553088, upload-time = "2025-12-24T10:27:31.325Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b1/daaab8046f56c60079648bd83774f61b283b59a9930a2f60790ee4cdedfe/faiss_cpu-1.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:c8b645e7d56591aa35dc75415bb53a62e4a494dba010e16f4b67daeffd830bd7", size = 18892621, upload-time = "2025-12-24T10:27:33.923Z" }, - { url = "https://files.pythonhosted.org/packages/06/6f/5eaf3e249c636e616ebb52e369a4a2f1d32b1caf9a611b4f917b3dd21423/faiss_cpu-1.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c", size = 8556374, upload-time = "2025-12-24T10:27:36.653Z" }, + { url = "https://files.pythonhosted.org/packages/59/68/20e91694ad9a8b2bb48af956899e52b645cb1501e7e2ec31cb733da4d4c5/faiss_cpu-1.15.0-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:50ea471ef1f4f3580eda8ab0ec9727d4bf65fd71c444bf306ce7cdbba8a42b21", size = 4904897, upload-time = "2026-08-03T17:49:37.003Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/ef4cf498977c4a84af7a8920bc97ca49fc19060c8464c63fab58847b4692/faiss_cpu-1.15.0-cp310-abi3-macosx_15_0_x86_64.whl", hash = "sha256:dd383bb1ce06fabcff5785f998f253aa88f88dcbe1fe36c922417cd6666dd896", size = 7087977, upload-time = "2026-08-03T17:49:38.947Z" }, + { url = "https://files.pythonhosted.org/packages/94/c8/88b072bf55714405d0d7e11c12349510f15a69ae56033b1cd894fb2be7d6/faiss_cpu-1.15.0-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d0a2d5d33fe023e263d0d355a837f20db67578e3be27fc5f4012a273274abf6", size = 9835009, upload-time = "2026-08-03T17:49:40.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3b/8878dbfc78a0084bbd408b34827a58b530be98132fcf620b7e15f9191614/faiss_cpu-1.15.0-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec9b29aae29e428c085c2d49dbb02e4673cdea75db418d420f9e60e0b4184498", size = 18764625, upload-time = "2026-08-03T17:49:43.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/2a/654116e6ee2808562a6b2a11c396bdb46d45689e3bf7206ee99400589cab/faiss_cpu-1.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30da3029952f0de69f16ce31946fd63fc3e292c867749bbcd2c0a0f09fd06f65", size = 11413863, upload-time = "2026-08-03T17:49:46.471Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/0a0f09659c1972aa83b9820cd3dd7f68f6678cfcfebde542e1c23d7d8663/faiss_cpu-1.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88fbe1acac6978869063cb2f9477f85718da596a6e0a17751618f9c756bce255", size = 19470092, upload-time = "2026-08-03T17:49:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b1/47967207659650ad74c1b06c42671e6beb4f7d798fe6eb2d53ba5e77ad90/faiss_cpu-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:90169515a95ea58a9a95d419e518907927a8ef54c46788396365ec5902c9c8df", size = 16247194, upload-time = "2026-08-03T17:49:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/021398ec5608314124b554bb025878a86f129bcf3576c293826352d9a783/faiss_cpu-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:5b940897b317febaa761088513a3db164fad3ac71a5e1ed7be9a052c9bf1a447", size = 16251530, upload-time = "2026-08-03T17:50:00.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/74/4a70395a6e07036628a1bd0b3f709101a6aecfa6a746db13b6e7921cf291/faiss_cpu-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:22dddb013e764aad66dac6cd15b49c7598d60339e0591b73b5e081629419c21b", size = 16251914, upload-time = "2026-08-03T17:50:03.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/13/0a021b9df16963f839a3f325657656b70f23b5a6dbeb422eaa187d0121b3/faiss_cpu-1.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:37170d5e9ead4b6bfd9c314afc39e17e92064068a0c5a4063dd3f39568c2667e", size = 16535739, upload-time = "2026-08-03T17:50:06.714Z" }, ] [[package]] @@ -604,15 +761,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.1" +version = "2.57.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, ] [package.optional-dependencies] @@ -622,7 +779,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.68.0" +version = "2.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -636,56 +793,72 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/2c/f059982dbcb658cc535c81bbcbe7e2c040d675f4b563b03cdb01018a4bc3/google_genai-1.68.0.tar.gz", hash = "sha256:ac30c0b8bc630f9372993a97e4a11dae0e36f2e10d7c55eacdca95a9fa14ca96", size = 511285, upload-time = "2026-03-18T01:03:18.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/f1/f2f31b2a6bd826bc2cb73068df880954a026474c03a4315637d20cc13965/google_genai-2.22.0.tar.gz", hash = "sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd", size = 684719, upload-time = "2026-09-02T18:06:02.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/de/7d3ee9c94b74c3578ea4f88d45e8de9405902f857932334d81e89bce3dfa/google_genai-1.68.0-py3-none-any.whl", hash = "sha256:a1bc9919c0e2ea2907d1e319b65471d3d6d58c54822039a249fe1323e4178d15", size = 750912, upload-time = "2026-03-18T01:03:15.983Z" }, + { url = "https://files.pythonhosted.org/packages/de/96/0120d214958cb54f2b9c48da9f564a0e6eae269e9dd702415fb1d0551cc7/google_genai-2.22.0-py3-none-any.whl", hash = "sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6", size = 1088792, upload-time = "2026-09-02T18:06:00.629Z" }, ] [[package]] name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, ] [[package]] @@ -710,6 +883,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -734,13 +920,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "idna" -version = "3.11" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -760,87 +972,88 @@ sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b7265 [[package]] name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] @@ -857,30 +1070,30 @@ wheels = [ [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "langchain" -version = "1.2.13" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/e5/56fdeedaa0ef1be3c53721d382d9e21c63930179567361610ea6102c04ea/langchain-1.2.13.tar.gz", hash = "sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4", size = 573819, upload-time = "2026-03-19T17:16:07.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/5d/0ef368fdcf5df08a394787196fb7a80f473a9c7b3fc4cd2e3b53a7a5853d/langchain-1.4.0.tar.gz", hash = "sha256:08da122a439f738f4c09a5158cfa3d2c1d8bea2d0bde7fbbf4aeb4d7f7fd9d80", size = 729354, upload-time = "2026-09-03T16:59:32.442Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1d/a509af07535d8f4621d77f3ba5ec846ee6d52c59d2239e1385ec3b29bf92/langchain-1.2.13-py3-none-any.whl", hash = "sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1", size = 112488, upload-time = "2026-03-19T17:16:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/7da07d9977a5e292ef602c1bb911514ff8206e85dc09a66961874345d57a/langchain-1.4.0-py3-none-any.whl", hash = "sha256:af1dc0161d30944a52ec7844d9bf890d0497b12ec0703069f3503b4003cbbac3", size = 161534, upload-time = "2026-09-03T16:59:31.154Z" }, ] [[package]] name = "langchain-classic" -version = "1.0.3" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -891,40 +1104,42 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/04/b01c09e37414bab9f209efa311502841a3c0de5bc6c35e729c8d8a9893c9/langchain_classic-1.0.3.tar.gz", hash = "sha256:168ef1dfbfb18cae5a9ff0accecc9413a5b5aa3464b53fa841561a3384b6324a", size = 10534933, upload-time = "2026-03-13T13:56:11.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/65/6b5e8a7ff2f2968652c88a67dcecb925b9d8f0a0ce9458c76cd5a0dbd138/langchain_classic-1.0.8.tar.gz", hash = "sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484", size = 10557071, upload-time = "2026-06-10T21:27:54.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/e6/cfdeedec0537ffbf5041773590d25beb7f2aa467cc6630e788c9c7c72c3e/langchain_classic-1.0.3-py3-none-any.whl", hash = "sha256:26df1ec9806b1fbff19d9085a747ea7d8d82d7e3fb1d25132859979de627ef79", size = 1041335, upload-time = "2026-03-13T13:56:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/99/9a/b8f5cb7490fdbf233088031fc69c9c747439d4097f67f196c1eb4869916d/langchain_classic-1.0.8-py3-none-any.whl", hash = "sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213", size = 1041536, upload-time = "2026-06-10T21:27:52.767Z" }, ] [[package]] name = "langchain-community" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, - { name = "dataclasses-json" }, { name = "httpx-sse" }, { name = "langchain-classic" }, { name = "langchain-core" }, { name = "langsmith" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "requests" }, { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/97/a03585d42b9bdb6fbd935282d6e3348b10322a24e6ce12d0c99eb461d9af/langchain_community-0.4.1.tar.gz", hash = "sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85", size = 33241144, upload-time = "2025-10-27T15:20:32.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/0c/e3aca1f2b1c5b95f8b87cb2b6e81a6f20d538c07a128419dc01cef0617b6/langchain_community-0.4.2.tar.gz", hash = "sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e", size = 33268708, upload-time = "2026-05-22T19:42:59.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a4/c4fde67f193401512337456cabc2148f2c43316e445f5decd9f8806e2992/langchain_community-0.4.1-py3-none-any.whl", hash = "sha256:2135abb2c7748a35c84613108f7ebf30f8505b18c3c18305ffaecfc7651f6c6a", size = 2533285, upload-time = "2025-10-27T15:20:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/5d97e42a3e95dc2a6d71b2f902a3fae71786131e11d01bddb604accb0ebe/langchain_community-0.4.2-py3-none-any.whl", hash = "sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466", size = 2364406, upload-time = "2026-05-22T19:42:57.103Z" }, ] [[package]] name = "langchain-core" -version = "1.2.20" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "httpx" }, { name = "jsonpatch" }, + { name = "langchain-protocol" }, { name = "langsmith" }, { name = "packaging" }, { name = "pydantic" }, @@ -933,26 +1148,38 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/41/6552a419fe549a79601e5a698d1d5ee2ca7fe93bb87fd624a16a8c1bdee3/langchain_core-1.2.20.tar.gz", hash = "sha256:c7ac8b976039b5832abb989fef058b88c270594ba331efc79e835df046e7dc44", size = 838330, upload-time = "2026-03-18T17:34:45.522Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/7b/406b4a8dd43dd01f69e04aafdee206809a2097134b2f944e12802eae7948/langchain_core-1.6.2.tar.gz", hash = "sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d", size = 1004390, upload-time = "2026-09-04T21:29:22.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/06/08c88ddd4d6766de4e6c43111ae8f3025df383d2a4379cb938fc571b49d4/langchain_core-1.2.20-py3-none-any.whl", hash = "sha256:b65ff678f3c3dc1f1b4d03a3af5ee3b8d51f9be5181d74eb53c6c11cd9dd5e68", size = 504215, upload-time = "2026-03-18T17:34:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/d3fb7c215cb2f237c3fe84880bf347a38deafef6033b6d5f1339ba8ca401/langchain_core-1.6.2-py3-none-any.whl", hash = "sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711", size = 571690, upload-time = "2026-09-04T21:29:21.175Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/56/913599f2f9cec8524868929f12d72b2ede377a6056ca8a40a32bdadfa535/langchain_protocol-0.0.19.tar.gz", hash = "sha256:79d90a1425122ac87e8052e2ec054fbd09c3edbf341bdfb6397112a495c7bf8c", size = 6265, upload-time = "2026-08-26T21:12:00.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/c9/f6cbf357d48ccbd18bb394433b1fd7ad9be004eed9377ad08bb85777e5e6/langchain_protocol-0.0.19-py3-none-any.whl", hash = "sha256:4cdf879a492a35980fd859ae792d3c65458ccaae504e183c9a10d7eac1f0720f", size = 7327, upload-time = "2026-08-26T21:11:59.781Z" }, ] [[package]] name = "langchain-text-splitters" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/38/14121ead61e0e75f79c3a35e5148ac7c2fe754a55f76eab3eed573269524/langchain_text_splitters-1.1.1.tar.gz", hash = "sha256:34861abe7c07d9e49d4dc852d0129e26b32738b60a74486853ec9b6d6a8e01d2", size = 279352, upload-time = "2026-02-18T23:02:42.798Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/66/d9e0c3b83b0ad75ee746c51ba347cacecb8d656b96e1d513f3e334d1ccab/langchain_text_splitters-1.1.1-py3-none-any.whl", hash = "sha256:5ed0d7bf314ba925041e7d7d17cd8b10f688300d5415fb26c29442f061e329dc", size = 35734, upload-time = "2026-02-18T23:02:41.913Z" }, + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, ] [[package]] name = "langgraph" -version = "1.1.3" +version = "1.2.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -962,68 +1189,192 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/b2/e7db624e8b0ee063ecfbf7acc09467c0836a05914a78e819dfb3744a0fac/langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370", size = 545120, upload-time = "2026-03-18T23:42:58.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f7/221cc479e95e03e260496616e5ce6fb50c1ea01472e3a5bc481a9b8a2f83/langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546", size = 168149, upload-time = "2026-03-18T23:42:56.967Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.0.1" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/e1/089c4c9e0a2fec7f883f82ae8e6a727138d50074cfeb6644bc2d13b1019b/langgraph_checkpoint-4.2.0.tar.gz", hash = "sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7", size = 180239, upload-time = "2026-08-07T20:05:03.756Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/3b475f09bd57d3a5649792c66353312b4432afd843f301739dfcebd157f0/langgraph_checkpoint-4.2.0-py3-none-any.whl", hash = "sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942", size = 56833, upload-time = "2026-08-07T20:05:02.655Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.8" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.12" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/a1/012f0e0f5c9fd26f92bdc9d244756ad673c428230156ef668e6ec7c18cee/langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71", size = 194932, upload-time = "2026-03-18T22:15:54.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4d/4f796e86b03878ab20d9b30aaed1ad459eda71a5c5b67f7cfe712f3548f2/langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327", size = 95834, upload-time = "2026-03-18T22:15:53.545Z" }, + { url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" }, ] [[package]] name = "langsmith" -version = "0.7.22" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx2" }, { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "packaging" }, { name = "pydantic" }, { name = "requests" }, { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, { name = "uuid-utils" }, + { name = "websockets" }, { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/2a/2d5e6c67396fd228670af278c4da7bd6db2b8d11deaf6f108490b6d3f561/langsmith-0.7.22.tar.gz", hash = "sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04", size = 1134923, upload-time = "2026-03-19T22:45:23.492Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/94/1f5d72655ab6534129540843776c40eff757387b88e798d8b3bf7e313fd4/langsmith-0.7.22-py3-none-any.whl", hash = "sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4", size = 359927, upload-time = "2026-03-19T22:45:21.603Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f4/b7/74d0992a461eacad8106e5dcfd7677d7640b5ff4e12ea545856467004f4f/langsmith-0.12.2.tar.gz", hash = "sha256:ff369ba4390e0969dbb109e838281404ae637e76ef73b347336bae05b4f87331", size = 4860106, upload-time = "2026-09-05T20:26:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e1/5c751a0f1a14de49f19a2ebc0f5dfadfaf04a07f245c5f749ec31f263715/langsmith-0.12.2-py3-none-any.whl", hash = "sha256:ca2b6386cf452cf8edebf27c9c476503c18bad895e7ad566578393fa0ba7fea2", size = 760692, upload-time = "2026-09-05T20:26:18.166Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] @@ -1031,7 +1382,7 @@ name = "macholib" version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "altgraph", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "altgraph" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ @@ -1040,26 +1391,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -1188,6 +1527,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "mypy" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" }, + { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, + { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, + { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1199,100 +1598,180 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, - { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, - { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, - { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, - { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, - { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, - { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, - { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, - { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, - { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, - { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, - { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, - { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, - { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, - { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, - { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, - { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, - { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, - { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, - { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, - { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, ] [[package]] name = "openai" -version = "2.29.0" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/4e/45c33ece533f9e4252de537c802e2efb68b066befc7b7bbbbc59c4865341/openai-3.10.0.tar.gz", hash = "sha256:c2dc841b944c4720a222f9a92f66a9a122e46968d87ed7857104655054c687ba", size = 1486030, upload-time = "2026-09-09T00:18:25.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0d796ada2d1222cfe609cf435ce7a688450735cee401b24737882c9b9612/openai-3.10.0-py3-none-any.whl", hash = "sha256:d1f307d866c7d12872809aeb68b7d1a085b49c2656787eae7c39ba381a41ad0d", size = 1746707, upload-time = "2026-09-09T00:18:22.498Z" }, ] [[package]] @@ -1309,70 +1788,65 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, ] [[package]] @@ -1425,11 +1899,20 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1452,122 +1935,134 @@ wheels = [ [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -1593,7 +2088,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1601,120 +2096,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, ] [[package]] @@ -1728,16 +2228,16 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyinstaller" -version = "6.19.0" +version = "6.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, @@ -1748,37 +2248,36 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/63/fd62472b6371d89dc138d40c36d87a50dc2de18a035803bbdc376b4ffac4/pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865", size = 4036072, upload-time = "2026-02-14T18:06:28.718Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/2b/836d9def811c02522e0921d8b8cdf0c16b0545a216e97e71041758057859/pyinstaller-6.22.2.tar.gz", hash = "sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd", size = 4092631, upload-time = "2026-08-17T20:53:22.231Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/eb/23374721fecfa72677e79800921cb6aceefa6ba48574dc404f3f6c6c3be7/pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134", size = 1040563, upload-time = "2026-02-14T18:05:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7e/dfd724b0b533f5aaec0ee5df406fe2319987ed6964480a706f85478b12ea/pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11", size = 735477, upload-time = "2026-02-14T18:05:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/88/c9/ee3a4101c31f26344e66896c73c1fd6ed8282bf871473365b7f8674af406/pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf", size = 747143, upload-time = "2026-02-14T18:05:31.488Z" }, - { url = "https://files.pythonhosted.org/packages/da/0a/fc77e9f861be8cf300ac37155f59cc92aff99b29f2ddd78546f563a5b5a6/pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122", size = 744849, upload-time = "2026-02-14T18:05:35.492Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e3/6872e020ee758afe0b821663858492c10745608b07150e5e2c824a5b3e1c/pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63", size = 741590, upload-time = "2026-02-14T18:05:39.59Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/b8db5f1a4b0fb228175f2ea0aa33f949adcc097fbe981cc524f9faf85777/pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe", size = 741448, upload-time = "2026-02-14T18:05:45.636Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/63b0600f2694e9141b83129fbc1c488ec84d5a0770b1448ec154dcd0fee9/pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83", size = 740613, upload-time = "2026-02-14T18:05:49.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/d4/e812ad36178093a0e9fd4b8127577748dd85b0cb71de912229dca21fd741/pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6", size = 740350, upload-time = "2026-02-14T18:05:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/52/03/b2c2ee41fb8e10fd2a45d21f5ec2ef25852cfb978dbf762972eed59e3d63/pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2", size = 1324317, upload-time = "2026-02-14T18:06:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d3/6d5e62b8270e2b53a6065e281b3a7785079b00e9019c8019952828dd1669/pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33", size = 1384894, upload-time = "2026-02-14T18:06:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/81/65/458cd523308a101a22fd2742893405030cc24994cc74b1b767cecf137160/pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea", size = 1325374, upload-time = "2026-02-14T18:06:12.804Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/08cd53632276de70426e7c273820277a48253fee397b4048301ec03c3566/pyinstaller-6.22.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d", size = 1062734, upload-time = "2026-08-17T20:52:17.635Z" }, + { url = "https://files.pythonhosted.org/packages/74/22/2d865896782cbb41e2388c7314207c17a98acdcc1b8e5eef668873505c9f/pyinstaller-6.22.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c", size = 755697, upload-time = "2026-08-17T20:52:21.676Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/6c11e4d5a76e716ea68da1946ab706a4bdf6d8e68b0e1dea314366d046a0/pyinstaller-6.22.2-py3-none-manylinux2014_i686.whl", hash = "sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d", size = 768870, upload-time = "2026-08-17T20:52:25.511Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e2/dbfea6a58b68acf644f7193b11d570476d6ffaed57381b6d1dd9e898a971/pyinstaller-6.22.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4", size = 767445, upload-time = "2026-08-17T20:52:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/f24a21af2f87ce1df4e07fdad95eec65ef9f284267a7c1e5eeea40f49aa4/pyinstaller-6.22.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4", size = 762113, upload-time = "2026-08-17T20:52:34.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/07/b304ff3f5f8333778065e3658b604b0e108cd934b920f3fbab825a0dd5b7/pyinstaller-6.22.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096", size = 762173, upload-time = "2026-08-17T20:52:38.477Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9b/0af69d93dfad2e3d590a5de5828d5bcd903747c0224bd9560ab476904dfe/pyinstaller-6.22.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1", size = 761674, upload-time = "2026-08-17T20:52:42.646Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4e/28b6094dcbd1e1bbb868daf4f8752999a20c1020ab64569232be42af2be8/pyinstaller-6.22.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0", size = 761053, upload-time = "2026-08-17T20:52:46.594Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2c/f91b63fd01422111ac04e3b9bf61c039da5a3292acb4fe5248905f0be688/pyinstaller-6.22.2-py3-none-win32.whl", hash = "sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9", size = 1344474, upload-time = "2026-08-17T20:52:53.108Z" }, + { url = "https://files.pythonhosted.org/packages/3f/53/8ba1d0f6159b490f700eac6161a4be5f0d4672608a6dae9fd73679f183ee/pyinstaller-6.22.2-py3-none-win_amd64.whl", hash = "sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7", size = 1405725, upload-time = "2026-08-17T20:52:59.667Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/9a3062cc34c939f694d4262c4754681f6ffe853c21c124209ad2d08b8e84/pyinstaller-6.22.2-py3-none-win_arm64.whl", hash = "sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e", size = 1353806, upload-time = "2026-08-17T20:53:06.222Z" }, ] [[package]] name = "pyinstaller-hooks-contrib" -version = "2026.3" +version = "2026.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, - { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/17/716326f6ba18d0663f7995ae369c23e50efebc22fbb054e9710a45688f61/pyinstaller_hooks_contrib-2026.3.tar.gz", hash = "sha256:800d3a198a49a6cd0de2d7fb795005fdca7a0222ed9cb47c0691abd1c27b9310", size = 172323, upload-time = "2026-03-09T22:44:06.345Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/60/d881fa1ba8c160c18d8e6f782bb16ec4640c08bc08fc50f704c368ad4f9e/pyinstaller_hooks_contrib-2026.7.tar.gz", hash = "sha256:5fbcaacb22c4f4aac869a127dce283f67a4b4cfcc37d496f2446603e6d68aefa", size = 175992, upload-time = "2026-08-24T21:26:58.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/19/781352446af28755f16ce52b2d97f7a6f2d7974ac34c00ca5cd8c40c9098/pyinstaller_hooks_contrib-2026.3-py3-none-any.whl", hash = "sha256:5ecd1068ad262afecadf07556279d2be52ca93a88b049fae17f1a2eb2969254a", size = 454625, upload-time = "2026-03-09T22:44:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/350377af7b50416344ab8792756d414eef7629c618a73e9a0b13bb1552d9/pyinstaller_hooks_contrib-2026.7-py3-none-any.whl", hash = "sha256:24257a04c7a5a7a034cf28e39dcee20fbeeb9f043076729480f2e1b69904408a", size = 459445, upload-time = "2026-08-24T21:26:57.274Z" }, ] [[package]] name = "pyrag-kit" -version = "1.3.0" +version = "1.4.0" source = { virtual = "." } dependencies = [ { name = "anthropic" }, @@ -1790,7 +2289,8 @@ dependencies = [ { name = "langchain-community" }, { name = "langchain-core" }, { name = "langchain-text-splitters" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "openai" }, { name = "openpyxl" }, { name = "prompt-toolkit" }, @@ -1808,46 +2308,52 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "bandit" }, + { name = "mypy" }, { name = "pyinstaller" }, { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.86.0" }, - { name = "faiss-cpu", specifier = ">=1.13.2" }, - { name = "google-genai", specifier = ">=1.68.0" }, + { name = "anthropic", specifier = ">=1.4.0" }, + { name = "faiss-cpu", specifier = ">=1.15.0" }, + { name = "google-genai", specifier = ">=2.22.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "jieba", specifier = ">=0.42.1" }, - { name = "langchain", specifier = ">=1.2.13" }, - { name = "langchain-community", specifier = ">=0.4.1" }, - { name = "langchain-core", specifier = ">=1.2.20" }, - { name = "langchain-text-splitters", specifier = ">=1.1.1" }, - { name = "numpy", specifier = ">=2.4.3" }, - { name = "openai", specifier = ">=2.29.0" }, + { name = "langchain", specifier = ">=1.4.0" }, + { name = "langchain-community", specifier = ">=0.4.2" }, + { name = "langchain-core", specifier = ">=1.6.2" }, + { name = "langchain-text-splitters", specifier = ">=1.1.2" }, + { name = "numpy", specifier = ">=2.4.6" }, + { name = "openai", specifier = ">=3.10.0" }, { name = "openpyxl", specifier = ">=3.1.5" }, - { name = "prompt-toolkit", specifier = ">=3.0.52" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pydantic-settings", specifier = ">=2.13.1" }, + { name = "prompt-toolkit", specifier = ">=3.0.53" }, + { name = "pydantic", specifier = ">=2.13.5" }, + { name = "pydantic-settings", specifier = ">=2.15.0" }, { name = "pyfiglet", specifier = ">=1.0.4" }, { name = "questionary", specifier = ">=2.1.1" }, { name = "rank-bm25", specifier = ">=0.2.2" }, - { name = "requests", specifier = ">=2.32.5" }, - { name = "rich", specifier = ">=14.3.3" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "rich", specifier = ">=15.0.0" }, { name = "tenacity", specifier = ">=9.1.4" }, - { name = "tiktoken", specifier = ">=0.12.0" }, - { name = "volcengine-python-sdk", extras = ["ark"], specifier = ">=5.0.18" }, + { name = "tiktoken", specifier = ">=0.14.0" }, + { name = "volcengine-python-sdk", extras = ["ark"], specifier = ">=5.0.48" }, ] [package.metadata.requires-dev] dev = [ - { name = "pyinstaller", specifier = ">=6.16.0" }, - { name = "pytest", specifier = ">=9.0.2" }, + { name = "bandit", specifier = ">=1.9.4" }, + { name = "mypy", specifier = ">=2.3.1" }, + { name = "pyinstaller", specifier = ">=6.22.2" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.16.6" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1856,9 +2362,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1875,11 +2381,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] @@ -1963,7 +2469,8 @@ name = "rank-bm25" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" } wheels = [ @@ -1972,111 +2479,127 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, - { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, - { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, - { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, - { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +version = "2026.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/fc/b4ff5d9550796f9f955b6c2470f37e2b582ec944638f3004499929a69c96/regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65", size = 493967, upload-time = "2026-09-01T00:50:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/ab60a24883b810a4b29065540f56e6f72590bb97d704d7e0ee7bcd2ba98c/regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4", size = 295227, upload-time = "2026-09-01T00:50:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/e3bf61522cd82eda85a90dd8f69c839f0ce1da64dc6e87f803bb79267c59/regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e", size = 290639, upload-time = "2026-09-01T00:50:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/e8492b054c5c9fe09bc0aefdc308b8f845ec2094b1ae779accec8f14dedb/regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328", size = 791712, upload-time = "2026-09-01T00:50:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/429ee3479bb438f7b2b41147fa68d63a7ef9c7486fcd3960e04e61133cf3/regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71", size = 861711, upload-time = "2026-09-01T00:50:06.988Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/6ebf3cec538ed49d0a4cf5729129fe39580ddfa80c1c59c4274651762202/regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e", size = 905894, upload-time = "2026-09-01T00:50:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/a7bb1e4038a170714c824dc7c739f8b911cb1172f87ab950be39e0691f95/regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01", size = 801135, upload-time = "2026-09-01T00:50:10.308Z" }, + { url = "https://files.pythonhosted.org/packages/bd/47/1b318fb249d5fa3dcb0375ad284d34155f0da4a2fe92c8f01edef0cb0258/regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141", size = 773578, upload-time = "2026-09-01T00:50:12.125Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/63f2644a0a3e92623f83ccadd04873916f5b650e2028bc44ee9df104a839/regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc", size = 783915, upload-time = "2026-09-01T00:50:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/f078963d1dbd3632f6e25f7d972cc9a9332649ed239995bfeb403cf88a3b/regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b", size = 854924, upload-time = "2026-09-01T00:50:15.826Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/457ac94a2a2aaa2ab5c076324dfd7e38e76f36d4d78923c4ddb8c3d93d75/regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124", size = 763172, upload-time = "2026-09-01T00:50:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/db145a6ad935cdc6c694381543d53e6e7d7212287d4f8885f965e91ea4a4/regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f", size = 844983, upload-time = "2026-09-01T00:50:19.572Z" }, + { url = "https://files.pythonhosted.org/packages/76/00/100346c5cc2384a7ce0adfb7dbb11f4a865c76e3a920fe68542042da66ea/regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce", size = 789532, upload-time = "2026-09-01T00:50:21.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/d878c1d3eed2768f19e8b6fa4aa81b0a0e6bc23fa34e64173ef48816844a/regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420", size = 266766, upload-time = "2026-09-01T00:50:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/da/6a0ac27bacb39aca3452cdd7d1bfc361608ad000cea3e1b0d7dbaa1e2b15/regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc", size = 278007, upload-time = "2026-09-01T00:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a2/b23997814f48f823bcda615764b4fb00d72a5a06bdbb41cafa895ec62428/regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8", size = 276960, upload-time = "2026-09-01T00:50:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, + { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, + { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, + { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, + { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, + { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, + { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a0/322f050f77289a1acf99b5c400fe201780bdcfa8f8876fbe250ae3de37af/regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886", size = 496701, upload-time = "2026-09-01T00:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/91/30/85df182aec58aaa34b02a3f0a99068bf34a7c9b3f9bd7ff06d58aa866e1d/regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6", size = 297055, upload-time = "2026-09-01T00:51:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/64b8bbd0316baaede047dbdbf2bcc96f10a4eeddec2e9cbf289e855e636f/regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7", size = 292008, upload-time = "2026-09-01T00:51:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/224614ef16bc8336269a84b578fb0dbab1433b584aaefc5f0680cacf9227/regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944", size = 796341, upload-time = "2026-09-01T00:51:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e1/10ad988d3b673db86353666c56cb32e1b6246ca1ab0c81018a58ebec889c/regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b", size = 866311, upload-time = "2026-09-01T00:51:31.592Z" }, + { url = "https://files.pythonhosted.org/packages/08/16/8d597f97bb6215876512cd7b3c47179c0eb79d1820ab2ed8412d3ba5599a/regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8", size = 911869, upload-time = "2026-09-01T00:51:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/f6bcb26472873b9308df329f507d9c6cb526e3f9aed847498f153f2b7539/regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec", size = 801375, upload-time = "2026-09-01T00:51:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/38c7cd736bfd58b190412dbf5a81aae1c38456c538d5c4987f5980cbfa3d/regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69", size = 776522, upload-time = "2026-09-01T00:51:37.81Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/8ecefa1c9b57b29fcbe963797435e69a3ba6a9773d802b3d4fca4c75ecf6/regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7", size = 785727, upload-time = "2026-09-01T00:51:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4c/af402c5aa95615ab66620d775de07ea09b0fea206cd79e3c8a5716d5fc96/regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c", size = 861375, upload-time = "2026-09-01T00:51:41.829Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7b/b83dd26470fa4cc6d7beee9b3c52ff0e988ba636c1367a04d4b2829d8d95/regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc", size = 766221, upload-time = "2026-09-01T00:51:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/94/5b/3b91b19d8fb11a353738cec56b19440bf181f3f9a0298d91140fb4061778/regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69", size = 851737, upload-time = "2026-09-01T00:51:46.708Z" }, + { url = "https://files.pythonhosted.org/packages/2e/be/ea0762fd8a12fa711f1a626283f6d79a3d8a81df4eaada4fbe82009e46f6/regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380", size = 789455, upload-time = "2026-09-01T00:51:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dd/b9af9d65de78831fb76cb8ef420db4094295b09f93f8abd17c867261c4fb/regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd", size = 272528, upload-time = "2026-09-01T00:51:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fe/ecb15616ae7aa4892299b9ca7c20ef0dd6e5c833643b7ed46e27ff5fcccd/regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846", size = 280812, upload-time = "2026-09-01T00:51:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/18/4f/60efc748152c82b967bd186c356dd1fc21c4f6c225b8026b52547ae60c9f/regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16", size = 281095, upload-time = "2026-09-01T00:51:55.052Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cc/aa269583e8986be9604ef5fb9e2164f33108f6220c896efff2dfb0d4e4c4/regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16", size = 501014, upload-time = "2026-09-01T00:51:56.902Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/bb8a3f3a47beccbf92c3bcda67c2c0336c13c2e10f9744306275f428ebc9/regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d", size = 299376, upload-time = "2026-09-01T00:51:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/c646df2b62c1b1334e28581715ed776814c8ac80c0c9106175f0c61ce12a/regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595", size = 294424, upload-time = "2026-09-01T00:52:01.113Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/3dd8c49cc4af440a9a1a0ebb2b4a939398398c81fb7fecbc3c7d534924a1/regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17", size = 811421, upload-time = "2026-09-01T00:52:03.289Z" }, + { url = "https://files.pythonhosted.org/packages/71/32/c35f031aec040b9136189e3f403681e3168f1f9862e31d9df92ea599f2c1/regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970", size = 870110, upload-time = "2026-09-01T00:52:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7b/931d962f9fbca3326f282a01fa5bed4d6734dab5215d1c20bac0ab85df58/regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3", size = 917489, upload-time = "2026-09-01T00:52:07.703Z" }, + { url = "https://files.pythonhosted.org/packages/46/1a/da0c28f2b1e65d46a24abc1dd33e0dc9ddbcfeeee7c5625103b3e09e548c/regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5", size = 817470, upload-time = "2026-09-01T00:52:09.686Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/6a540ec40c3b8eeefec445362cf08bbb686b22d4088f97d3c1ea38323434/regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181", size = 784742, upload-time = "2026-09-01T00:52:11.778Z" }, + { url = "https://files.pythonhosted.org/packages/fc/88/75500ca27ba85256eb46119ebd99ec10ead4c3ffacd47fe7dfe034662960/regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef", size = 800609, upload-time = "2026-09-01T00:52:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/18541aaad8b88977e2c3fbbb53c1a75f6f6c999800a82d2f7306581cb6b6/regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae", size = 864798, upload-time = "2026-09-01T00:52:16.137Z" }, + { url = "https://files.pythonhosted.org/packages/16/62/ffbfbeb5a3f2cca701afb9f8aebbfcbf37a4aae2f1584c96680142aab445/regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88", size = 772892, upload-time = "2026-09-01T00:52:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/04e2db28f490b1fcc684428bd2de21e1ec46f7d8d421b7f2cc74d120affd/regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177", size = 858065, upload-time = "2026-09-01T00:52:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/c0cac930d1dc0b86c5d8ff10602f15df4fb15fa3f33e04a98be197f33618/regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e", size = 804739, upload-time = "2026-09-01T00:52:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/69e6f4d02e11a5cb45033abede6e04b0da3716b1b6c311d4609be14a01a7/regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f", size = 274514, upload-time = "2026-09-01T00:52:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/28/64/064b78028b52c0c21b9c0b362b1bdcee7179f009891ddf959d9b6390b30c/regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16", size = 283797, upload-time = "2026-09-01T00:52:27.358Z" }, + { url = "https://files.pythonhosted.org/packages/f2/46/fbb41eb6cc92403ba9f896dfd56a1d5d31549c4033dfb7418da7ecd5802a/regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5", size = 283444, upload-time = "2026-09-01T00:52:29.632Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1a/825beb1c803d579d1119723235ede2211949a8d320bcc79e3f6e5dd6090f/regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9", size = 496929, upload-time = "2026-09-01T00:52:31.593Z" }, + { url = "https://files.pythonhosted.org/packages/ff/54/9b8255521680d54e1d157ed6532e3082998f47925d2ab3861550e0e89455/regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361", size = 297050, upload-time = "2026-09-01T00:52:33.459Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0e4df28fd94304050efc2373dff1803e5d62056cf744a694eaa376df2fe8/regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab", size = 292296, upload-time = "2026-09-01T00:52:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/689a8403f309a7a3f3f3e74b3e81b144602481d122c9e4070a601367f8e0/regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5", size = 798376, upload-time = "2026-09-01T00:52:37.235Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a9/3d22278a38bb8adb254d345e60935316222091caf00a4233aaef388d636e/regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be", size = 866607, upload-time = "2026-09-01T00:52:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/69/ef/ecf8c5fdcec73ac0526fbd074be3bd1059b11cbda739ac0ab9001f311aed/regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e", size = 912160, upload-time = "2026-09-01T00:52:41.603Z" }, + { url = "https://files.pythonhosted.org/packages/68/e8/c07c3d2b44c9154672a33911cfcfba626c78d939d7e31103864d81ad319a/regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb", size = 804589, upload-time = "2026-09-01T00:52:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/d42957c78b5934efd03ca5713d99952cbe71e2d0b3b24d9e78256f208587/regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f", size = 784446, upload-time = "2026-09-01T00:52:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/4252dff33a1a5afb3271fb3859e992d34c47c040ecfc27bbfe6a7eaf99a8/regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c", size = 787996, upload-time = "2026-09-01T00:52:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/90d31048bb402fd746a95922d7dfdb9de9f902c39c8d838f2fef05c9c92c/regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27", size = 861752, upload-time = "2026-09-01T00:52:50.597Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/42ad133550cbd64d8fb33ec5ff55e1f524b06c6328e9f3bf9c3e8652b525/regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7", size = 773488, upload-time = "2026-09-01T00:52:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/78/8f/0c90e8fa607c36a65c360f6b02980aa7aa5885afa3d737daba4229bef086/regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253", size = 852603, upload-time = "2026-09-01T00:52:55.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bc/e1be309afb6be94916d8f4f000dbc66040fc0da2008b3f714dc4893259fd/regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20", size = 794635, upload-time = "2026-09-01T00:52:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/15f6d0ea137316531c992e7468a4c6f6e39159a6780c2c16ec3ab56f2149/regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae", size = 272530, upload-time = "2026-09-01T00:52:59.644Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/739168818790278dd4f1ba60c582fed41323508d7a8e850323851de1aa1f/regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57", size = 280822, upload-time = "2026-09-01T00:53:02.57Z" }, + { url = "https://files.pythonhosted.org/packages/12/6a/55f5f6e92a8c7d6511e069d3e8a4b45e3506941c1a97dddf5a329b132be4/regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950", size = 281102, upload-time = "2026-09-01T00:53:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/adaf9ed10ffdef60684bf8757c257400500342503f1ce5c7cffcdb2e8aaf/regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4", size = 501333, upload-time = "2026-09-01T00:53:06.681Z" }, + { url = "https://files.pythonhosted.org/packages/64/dd/2cbc3b253a88fdb0c8ca641367dc2f280541fdf1722f201fc0be62797577/regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff", size = 299273, upload-time = "2026-09-01T00:53:09.002Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/4818d31ec2555309b3935d3cd006a7f6076e11bb21eec9e72afee3db87b1/regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76", size = 294888, upload-time = "2026-09-01T00:53:11.009Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/8970a4102a067e0a28e40a232bf536752c373da8a4f932755679f77fa9ac/regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96", size = 813072, upload-time = "2026-09-01T00:53:13.566Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/2e52f5e9e92b79e324c9807e977fa6f9ab210b41ab5b7356307d5ffb2f4c/regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35", size = 869947, upload-time = "2026-09-01T00:53:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/c676e3f2dcb47ec941fa678019044cbac51db9441e09cec47392e4986013/regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa", size = 915678, upload-time = "2026-09-01T00:53:18.384Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/73c971bcaf1d24e73aa3c8e0f767caccfdc9fcda021e59f28f6dcd12728b/regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23", size = 817714, upload-time = "2026-09-01T00:53:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/0e9bffb6cfa8a00e765eaea0d1e6d207bb98e620d5658c537c8d87df671c/regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec", size = 792411, upload-time = "2026-09-01T00:53:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/61/d4/b1d226e8a3d0c8aac577a517384ffaea7b9ff98b194ddbd0bef58b9d8738/regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de", size = 802521, upload-time = "2026-09-01T00:53:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/dc47f94a4ace898561dfafeed6052dd3551915f36aa1971f4387d6daf0d4/regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f", size = 865266, upload-time = "2026-09-01T00:53:27.667Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/743565fc25cb36981186b4e9b42b3d093a7226cca328380cfd223c1b5a03/regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544", size = 780298, upload-time = "2026-09-01T00:53:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/d62e48324632eb0bae2c336da5810b51a14a9e23ba964f2828cc1ac23011/regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092", size = 858045, upload-time = "2026-09-01T00:53:32.397Z" }, + { url = "https://files.pythonhosted.org/packages/2e/41/8eff42a2516494bb6b36d33b15bcd861764dcc836ad7c0b746e46c513af9/regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69", size = 805406, upload-time = "2026-09-01T00:53:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1d/98198fd6dc185a172f5de48b8a4c7ca03c0d4285b1c1c32300672e7cf85a/regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a", size = 274674, upload-time = "2026-09-01T00:53:36.77Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/dc0e76842ccddc8756471adff38c140d130c6b601b9ae5724274f1881e7a/regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803", size = 283692, upload-time = "2026-09-01T00:53:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/da/3d/e0677f590f3473a3defb63ef6e3469154382a2a15ebafb364482d7ed3b01/regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2", size = 283421, upload-time = "2026-09-01T00:53:41.393Z" }, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2084,9 +2607,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -2103,24 +2626,49 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] name = "setuptools" -version = "82.0.1" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] @@ -2143,55 +2691,53 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/410fbc6c2f1fa8310f4ef1b6847d47d0ac1c042c7b4e81eaaca063d030a9/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43", size = 3306991, upload-time = "2026-08-11T21:00:08.603Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/25ffd5c24681ea4b46e62c80ceca8200ce204de1773366321306cf3f608a/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736", size = 3327454, upload-time = "2026-08-11T21:05:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f1/0f1b1d4800e51218e736a06ed55a3b2a59c257600bbaca7673bf13d2dbec/sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72", size = 2131248, upload-time = "2026-08-11T21:09:50.765Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/04d2ac5ad66f3d31278f37064ed5f5ef3fe653f7bdaa67036663f223d186/sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc", size = 2156943, upload-time = "2026-08-11T21:09:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[[package]] +name = "stevedore" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/97/bba6e7ec2f5498b9dcb7b1b6400086b80ae5a8ebaff4b25e8c8add75f439/stevedore-5.9.1-py3-none-any.whl", hash = "sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf", size = 54931, upload-time = "2026-08-20T15:25:13.602Z" }, ] [[package]] @@ -2205,145 +2751,195 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/66/62/167a842aa0429d45f5e797354fd4343a96f6043d67d0513c675c7b8d36e6/tiktoken-0.14.0.tar.gz", hash = "sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874", size = 38898, upload-time = "2026-08-17T19:49:49.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/c5/9d848b7f408241171e1f843deb8bfa626086452bc9c78beee500829583e3/tiktoken-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79", size = 1094971, upload-time = "2026-08-17T19:48:40.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a9/d94302340304328961d6f0c35ca4e60617fbb57a5cf667e2ed1692cb9e57/tiktoken-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948", size = 1042916, upload-time = "2026-08-17T19:48:41.541Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b6/31da98ee871383509cae2ba96a9ddef1965e3c4f8cb6dc7bcda3379398db/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f", size = 1188650, upload-time = "2026-08-17T19:48:42.729Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/8c5dddd7cb67f6571d154a58d7c6e2f07da54bf84c49b6a1839965b7c35e/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513", size = 1206378, upload-time = "2026-08-17T19:48:44.013Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/522ec59d30dd9a2f3ab837011cd4fc5d1178dc4a2fa07c9fa4b90af6ba9d/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78", size = 1253694, upload-time = "2026-08-17T19:48:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/9019e272bad188a1c61ecf44f25a9ba2368744644e3ac1f3d6516f3c9e80/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e", size = 1317873, upload-time = "2026-08-17T19:48:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/fff1217240343c0c11b5938b98aeae0e3a266cacfac25f86f91cdcd748f0/tiktoken-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da", size = 944395, upload-time = "2026-08-17T19:48:48.028Z" }, + { url = "https://files.pythonhosted.org/packages/8c/da/e273746b9d24a63c776bc60fba914351573ad9c575b52601eb5e60632564/tiktoken-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36", size = 1094408, upload-time = "2026-08-17T19:48:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/69/9f/fe6b1aca23331aa5271df5a4bd07bf68a7059254d47faee1b8272592a777/tiktoken-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4", size = 1038499, upload-time = "2026-08-17T19:48:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/e9f47647c9e163bd1de30fe1a491669b7248cfc67b7404c35c009a701e1a/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6", size = 1186355, upload-time = "2026-08-17T19:48:51.93Z" }, + { url = "https://files.pythonhosted.org/packages/51/11/9976ad86980a00cdef05e730a0127a2578a1bc6d11644d8d47246de2eb26/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d", size = 1204197, upload-time = "2026-08-17T19:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9c/7035b0bcfaa68d1ee4803fc5be5214ad865669b05bd20e7105ae8a18afc6/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482", size = 1250635, upload-time = "2026-08-17T19:48:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/69cabf18bed7f4366da076735816abce0d4db3fae491ae338a6612128777/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6", size = 1316085, upload-time = "2026-08-17T19:48:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/bd/bd/a2e884fb1402cba5be08836590320012b2d8ada0e2eef9911a64df4bcd2d/tiktoken-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3", size = 941208, upload-time = "2026-08-17T19:48:56.938Z" }, + { url = "https://files.pythonhosted.org/packages/50/53/ee1453623bf65f019328721ccb6587846d2c5b7b82f34e73ca09101f072e/tiktoken-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f", size = 1094198, upload-time = "2026-08-17T19:48:57.955Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/6448cfe278c3664ba9ec5b5ac08344341f7dc3d42888476e215a14eda2be/tiktoken-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94", size = 1038820, upload-time = "2026-08-17T19:48:59.015Z" }, + { url = "https://files.pythonhosted.org/packages/69/3b/d67eac1bcce9dee3abe23aff5e3ded3116bbebaf67b80a0811c06d3806fc/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06", size = 1186175, upload-time = "2026-08-17T19:49:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/37/62/cae690d9783146b0f81f564ada0f8f611de68178c0c9c7e1e969f0516b48/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d", size = 1203884, upload-time = "2026-08-17T19:49:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1e/633e30237b94e383cf814145499079f3bb9cdd4aeafc1bc42e01b0f810a6/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010", size = 1250980, upload-time = "2026-08-17T19:49:02.274Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/4c12f07b812f84206f38d723eb1ebfdd34bad9309b5dbc0bee6bbcff4cbf/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632", size = 1315434, upload-time = "2026-08-17T19:49:03.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e0/c65603f0c44811def666d3fbf611bf2af3b5e1ef613e06c19411419830b3/tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1", size = 940883, upload-time = "2026-08-17T19:49:04.583Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/1cf129f4af8fc513931f931023def596b7c4bfc77026513cd9d851da9e88/tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450", size = 1096273, upload-time = "2026-08-17T19:49:05.807Z" }, + { url = "https://files.pythonhosted.org/packages/62/85/2ae74575e321148484147e10b53c3b1717c59ebaa9edb4fe18b1f5c055f8/tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b", size = 1040269, upload-time = "2026-08-17T19:49:06.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/92a1120a12e4bcf2d5464350d1a91b68a433d63ce656bb7f806c27aec09c/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e", size = 1186101, upload-time = "2026-08-17T19:49:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7d/144af98dc5ad68108451a82e2f5a17f80e2663f5115058b8dfd215c1ad02/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42", size = 1204457, upload-time = "2026-08-17T19:49:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1f/be7cb06ab2108f612f3e92e7b76cf391e192db0db37a984616f0cc32aafc/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c", size = 1251716, upload-time = "2026-08-17T19:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6b/81f158d0f90adb826cd704069c2129a046cb784a2a09861009519fc41cf4/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771", size = 1315432, upload-time = "2026-08-17T19:49:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/f5fa35ec13f07279fdcaf3cc9c04bbb154ea591d23978651f2b672593e8a/tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098", size = 988046, upload-time = "2026-08-17T19:49:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/68/c9/7756717408d3d0dfea3f046c9466144b28afde39ff69d5808f2475dcd7f5/tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438", size = 1096261, upload-time = "2026-08-17T19:49:14.351Z" }, + { url = "https://files.pythonhosted.org/packages/79/29/46ad8061f57bd9f8b2ea0aa82bf574e0f2aa040b0857a1582adba9957899/tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa", size = 1040183, upload-time = "2026-08-17T19:49:15.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/3184d17b868456f17b60b1a75f5ec0405618a43aa753336df341d8f11781/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037", size = 1186719, upload-time = "2026-08-17T19:49:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/46de4400d5bf859f640feee85bd7e32235f68ddf25db53c63be78e581e3a/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef", size = 1204660, upload-time = "2026-08-17T19:49:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, + { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, - { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, - { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/b2/8f03b61f0aa4afc687855c4f00db35f4d3e58c480cd885abc46f6e41308f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371", size = 563901, upload-time = "2026-07-09T13:48:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/88b909ffb9ac11f88d2e6ceabc592ccc660b5830b06dbcbd290ab8981f1f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2", size = 286383, upload-time = "2026-07-09T13:48:10.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/bc5b64e9898867227c535cd0366c571c580a736748e81329437c1773e442/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479", size = 323244, upload-time = "2026-07-09T13:48:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/13/d9/8a17462ce066fbf89670fb737a3f0c93a77816736d2a4d134787e759d8ea/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1", size = 330466, upload-time = "2026-07-09T13:48:13.092Z" }, + { url = "https://files.pythonhosted.org/packages/43/37/0c65d0db3bae45183419756d938f1791a82c835fd92bf234eb4f008d2e02/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f", size = 443806, upload-time = "2026-07-09T13:48:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/7e698466d1f5254620b5ee0d711fdd20a0e9c2acd7040740c37193a8f673/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46", size = 324261, upload-time = "2026-07-09T13:48:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/5d/48/3a5b242d7f0b8e3ca77dcd7177f3cf73e0280cee32e2349d9796ca27f183/uuid_utils-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0", size = 350657, upload-time = "2026-07-09T13:48:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/f32ea82a89efed2eafee2f1d925d64687a81e550a9951933fb1b75c95ca6/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7", size = 500613, upload-time = "2026-07-09T13:48:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5c/c7b73ec4bbe28db162a4841d352c6eda582801e0dd9fe72f6ad5cc584ee4/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803", size = 606306, upload-time = "2026-07-09T13:48:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/63/95/8a2777204e8691b4961e6aa619001c3e5175aa430ab43da3079142e8d310/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb", size = 567231, upload-time = "2026-07-09T13:48:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/1d778ca3ed6d2cf35f22088e2de714675416747ab41be510f22c141043a7/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5", size = 529373, upload-time = "2026-07-09T13:48:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/9ad1ab64b3bed0a0237d1db89dc6f5001d6116a82766753da4ac4496f979/uuid_utils-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13", size = 169930, upload-time = "2026-07-09T13:48:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/e01417f52eae6e2cb412260bb332b4ee4b37af2982d9c38cff4b68b2e899/uuid_utils-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70", size = 177242, upload-time = "2026-07-09T13:48:24.723Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/396c27f996add19f8ac31e49cc4570824e51a97719087dabf94694d25bc4/uuid_utils-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2", size = 177023, upload-time = "2026-07-09T13:48:25.834Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/e2fd3fdf356e1b55d2acf1b956b4f3f29ffb215a99c387eba04b1c5fba66/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd", size = 562232, upload-time = "2026-07-09T13:49:06.201Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/65e0980d668a6d44e699f59d1acf43d6b5d4893592c115ce7c680bb4dfa1/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a", size = 287858, upload-time = "2026-07-09T13:49:07.45Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/5e97bcebc90fb6a10f98af3dc1ba552e04183aba59e2edc0b9cf486dd998/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc", size = 321587, upload-time = "2026-07-09T13:49:09.489Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/88b2a2370cc3d455ba0515fb6f5c8f7ac0c0f55a86801b6e56a432f22c17/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d", size = 328964, upload-time = "2026-07-09T13:49:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/181c5da673953dfc0958cb4fb3a4984a9098673ddb05cac68e994bc8511b/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7", size = 442909, upload-time = "2026-07-09T13:49:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/5c5e665af542884a8fd3c61725c38453239e13940326b5b70f3ef8881a97/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4", size = 323076, upload-time = "2026-07-09T13:49:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/f5/35/7de97de18cbf226c2a4f2104ad15e56ca4491717c81c0b71795c0c585b4e/uuid_utils-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099", size = 347360, upload-time = "2026-07-09T13:49:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/26/a1/9915d5dd59fdd1957ded5d188c0ea0b9db5a1d84d42c8d8828a7b83b366e/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354", size = 499267, upload-time = "2026-07-09T13:49:16.774Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/88108405262ec850cea0f95733445d6873e5772af3292baabd9ef8457740/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330", size = 604940, upload-time = "2026-07-09T13:49:18.147Z" }, + { url = "https://files.pythonhosted.org/packages/89/d5/6dbcd300de47cc443cff2656cd5327a385751213dcb2101cfee7388170b2/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0", size = 564172, upload-time = "2026-07-09T13:49:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ab/94/e8057f2288a415fba8a978bca4b589f5cb6b91a028a5dc07a1775938b33f/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5", size = 528533, upload-time = "2026-07-09T13:49:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6b/31713148c77e48e62f51aa042a98a54a8be0396912ea5130f83f52ae722d/uuid_utils-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0", size = 99197, upload-time = "2026-07-09T13:49:22.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f3/ca6f6ac5428312df8ed632f6dd9f9e6aba23090471fcdeae53eab027e8b3/uuid_utils-0.17.0-cp314-cp314-win32.whl", hash = "sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a", size = 169540, upload-time = "2026-07-09T13:49:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cd/7ede0db66411fa09817d79b680f7454ea9bee2d374e1922e4efd065760a3/uuid_utils-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0", size = 175984, upload-time = "2026-07-09T13:49:24.703Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/533b5f80cd4918c0693f4e1b7b90ceb1caa45f4266ae8b528135d7ecca5d/uuid_utils-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae", size = 174749, upload-time = "2026-07-09T13:49:25.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/f400ac39d06fd8be5b099c09e41bb975205926722a3e8d53348817cb7ff9/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0", size = 562610, upload-time = "2026-07-09T13:49:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/c71c8312304c56f6d0bcba87cd402fa79bec35d18ffc8c41954196ca68e5/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b", size = 289473, upload-time = "2026-07-09T13:49:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cd/522117e2e5184ca1d4f0f85ee833e9e21bd8c6b99eff8a4d1a8e5a194e33/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750", size = 321600, upload-time = "2026-07-09T13:49:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/0d81f9bd346fc717bc561c08fa6457e0328966eb76e536b938fe77d56459/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912", size = 329569, upload-time = "2026-07-09T13:49:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/26e1363f36a94c9e8ec2dd21d5f63088d3e7c723adbb12dcc8fdc77be417/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa", size = 442051, upload-time = "2026-07-09T13:49:33.024Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/2c1ed1b34d7df7fdcc11c28fd26d94d44843b37d9af2435ff9fd8abdbc08/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2", size = 324372, upload-time = "2026-07-09T13:49:34.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/bf/328d3c6bb22c496944a1b3b732207d71aa6964eb604e5e3b9dcb91ed0a00/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354", size = 348548, upload-time = "2026-07-09T13:49:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/a07de5cb7b90582fdbbc830fd19be129cbbb9897cfe239fef469d7bd2d09/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6", size = 498985, upload-time = "2026-07-09T13:49:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/f4/62/9966e46ae34fcec6b06119631fb3c09705ea78835035ce3a82d3348eb61a/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68", size = 605183, upload-time = "2026-07-09T13:49:38.648Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4e/bb962ba0fe31e903b199f22cf4c1a6cba35a8987aef526d287277ab8ca8b/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3", size = 565412, upload-time = "2026-07-09T13:49:40.115Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/122adfeeeae8a84ccfd43bce627b104d12a2180a93bffd2c0e1b54dad7a6/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd", size = 529885, upload-time = "2026-07-09T13:49:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/257304dded339dc35fc9bf35722ac68fd4fdb930f255b8f7bccdf74ebba9/uuid_utils-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91", size = 169472, upload-time = "2026-07-09T13:49:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/e78c06db7e9ce317ce7b8759ff2058333eac75caa8c22b75f0059589c9be/uuid_utils-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab", size = 176271, upload-time = "2026-07-09T13:49:44.105Z" }, + { url = "https://files.pythonhosted.org/packages/a7/11/bd1c70e1ad3301163cebe66c8d26de26e6814d52f642a849448bd2833626/uuid_utils-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9", size = 175004, upload-time = "2026-07-09T13:49:45.591Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/4ae708968b15cac7b68d5b854bfce724b21faa1c7a5147fb96d87f468a45/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf", size = 567823, upload-time = "2026-07-09T13:49:46.902Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e2/d3af9c3d1dc6efb9ee1cffab30f3f2aacacc3892b21b495d78d34c6696bc/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb", size = 288763, upload-time = "2026-07-09T13:49:48.491Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/f1b183e412387529893015a94a8447633c665f6d0392de20e245680e636a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343", size = 324919, upload-time = "2026-07-09T13:49:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3c/d32c799bdd51f3b08b6ee95f9de921b59c69075a96767f937fab55014813/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1", size = 332689, upload-time = "2026-07-09T13:49:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/6f/90/b4cd455619ff276dc3c3262a7420ead63aa1e531362f00df4cdb07d90e0a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec", size = 445726, upload-time = "2026-07-09T13:49:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f1/5cc042a37932aa9a66eb8ab4a9a5b31d80261ae4565ff0193d8cc1fb9392/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391", size = 325610, upload-time = "2026-07-09T13:49:54.191Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/9e800c41d766484484e97845a7a7f677ba94462df86c97183e0290229d16/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a", size = 352672, upload-time = "2026-07-09T13:49:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" }, ] [[package]] name = "volcengine-python-sdk" -version = "5.0.18" +version = "5.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2351,9 +2947,9 @@ dependencies = [ { name = "six" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/bc/15bc52f304c4d9762fa03db17527636d1b9b5d31617f371df6420e17a8d0/volcengine_python_sdk-5.0.18.tar.gz", hash = "sha256:b86ea55855aa8f1ca5baa92da7f4d1050b8c4b96762808f8e1aab49a2c5b0621", size = 8276904, upload-time = "2026-03-19T12:09:09.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/26/33282a2954cfd0cac2f596846953cb04ccbc6fe6e439499fe993c86c19fc/volcengine_python_sdk-5.0.48.tar.gz", hash = "sha256:bb78995e35c34ad7a24e535826a1cb197e1209783c8a7dc11b001a09cb4bc48c", size = 10754352, upload-time = "2026-09-03T12:18:26.556Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3a/51a8b9a58854585b963a2f6a8c2f04e7ff875f4904de339554f2a28a1d96/volcengine_python_sdk-5.0.18-py2.py3-none-any.whl", hash = "sha256:ea36abcd786ebb10d21ebd1df239f8384feede952cd98f1642461562d7c8e926", size = 32542954, upload-time = "2026-03-19T12:09:02.951Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/29eefbbdae397908c8a05eee73208229065481f2d797f5f2c720a6a02193/volcengine_python_sdk-5.0.48-py2.py3-none-any.whl", hash = "sha256:05c15f9138e016e479cdf29971d1e5fedbb9e8f4378930c68e2a9622d087995c", size = 43152725, upload-time = "2026-09-03T12:18:20.217Z" }, ] [package.optional-dependencies] @@ -2366,295 +2962,380 @@ ark = [ [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" }, ] [[package]] name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a5/1386f35da1475fcaeef42581deae73417c6d2a6a0b2d2e8914de18844dcd/xxhash-4.0.1.tar.gz", hash = "sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7", size = 101513, upload-time = "2026-08-17T08:24:08.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/58/bc81e25cceab76ce4b400441e3a43312bf3887fedbb2e5f80cc5a7dd7f75/xxhash-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c", size = 38473, upload-time = "2026-08-17T08:20:34.303Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/d95e810c9a2930906f1fbd0e38f77554abb8e042a190aa9cbb24da39e9a2/xxhash-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c", size = 36228, upload-time = "2026-08-17T08:20:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/be/ebcded2a32ba664a17a1737a91b6baa298bcda6942acdad81af81660b74c/xxhash-4.0.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794", size = 253292, upload-time = "2026-08-17T08:21:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/72d31d787d988d1130253bc34d249c553f02c9387e7dda789b6d5aa7963b/xxhash-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286", size = 276545, upload-time = "2026-08-17T08:20:26.726Z" }, + { url = "https://files.pythonhosted.org/packages/39/ae/048f3b1f283a340bef3697fe5a9d4f10de1696a379af9af6b2352c24d82a/xxhash-4.0.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf", size = 296295, upload-time = "2026-08-17T08:20:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/e9a593c81445c8e8d669402edb8264144624e7e5e2406df7d33e3c164d90/xxhash-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191", size = 279966, upload-time = "2026-08-17T08:21:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/29da2b166955a300521c0abd498ad4481916c3743949b106192b781f58fc/xxhash-4.0.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa", size = 509850, upload-time = "2026-08-17T08:20:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/ff/e39e1900179ce7f0f23e7000d9fc672471a8d05b6b2dc657cb496c8975d0/xxhash-4.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24", size = 261005, upload-time = "2026-08-17T08:20:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/89/79/76ee26720d13219458f4b2ec0b22f539fe2bcb1f83dc22a24be4cda4e285/xxhash-4.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79", size = 339620, upload-time = "2026-08-17T08:20:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c7/56b53252d64ecb2f3a0d283b41033561b1bec9c268095150153b694c2e52/xxhash-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c", size = 272558, upload-time = "2026-08-17T08:21:33.883Z" }, + { url = "https://files.pythonhosted.org/packages/fc/76/83101a2f2ad3eb6b4b5d571e0aa394a576e3e23121707d507d80197ac385/xxhash-4.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c", size = 300417, upload-time = "2026-08-17T08:20:36.183Z" }, + { url = "https://files.pythonhosted.org/packages/34/5d/5be1166ec4fc4bc896e508dc51189a2dad200b373269a430e214fb152693/xxhash-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a", size = 259286, upload-time = "2026-08-17T08:20:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/77/6c/42f6201f13278787c58a6b3a1cd597b47e8665a7d4f68a3440d001c05a75/xxhash-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1", size = 278230, upload-time = "2026-08-17T08:21:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/3cb7f149a5a469c628604c33bec6d79764698b315700a4bbf1c6fb15622b/xxhash-4.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925", size = 329846, upload-time = "2026-08-17T08:35:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ee/934eb1e11f4d0e95f3828825f8da4b6d59e338235d63edc3d929769262d2/xxhash-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df", size = 477285, upload-time = "2026-08-17T08:20:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/77835dcd7aec970db74f5724c94207ada83b3e2f5e1474ab44b9c8fe5ea5/xxhash-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a", size = 257552, upload-time = "2026-08-17T08:20:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a3ce7a1a9c4ec9ad8babba591a996a71c474ff9630c5fb04c8c9d8b995ca/xxhash-4.0.1-cp311-cp311-win32.whl", hash = "sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399", size = 34630, upload-time = "2026-08-17T08:21:53.323Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/60d2170e8cda0891aab25dcaa74797b420c3c6cde8a5bc8372f17b30c0cf/xxhash-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0", size = 36992, upload-time = "2026-08-17T08:20:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/b229a39f9bbbe30cbcf9afaaa8993cb65286715c90a3b058c3769196ae02/xxhash-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e", size = 33289, upload-time = "2026-08-17T08:20:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/26/6c/dc7cffeadd06336cd934947187cd38abb263103bbc552ca0f55fe4ff595a/xxhash-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428", size = 38444, upload-time = "2026-08-17T08:21:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/c9/cf736f6db8c3273af18925061572db0d4357818a9ce425f4b5fb0021918e/xxhash-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc", size = 36195, upload-time = "2026-08-17T08:35:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/da/a2/ca1929354b6851529d0148f7f335b5e2b0281f83bab3e19f0896dc579796/xxhash-4.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451", size = 253113, upload-time = "2026-08-17T08:20:52.152Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/542005206af59518bc8d78a210f1e0172217bc53beb32f64a5b632e72b6b/xxhash-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e", size = 276525, upload-time = "2026-08-17T08:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/1b/df/607cff25dcb0f1d35c3b04493f6ad8471edb03fd4eacbdcc5ceddef1f3e9/xxhash-4.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684", size = 297703, upload-time = "2026-08-17T08:21:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/15/ba/9d2275eea0b9d9c6b02921be23f7588356c60df95c763b25f0e045894d43/xxhash-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec", size = 280252, upload-time = "2026-08-17T08:20:47.299Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/2299d9f6369e550aef2abb64945e39daa34412725aa46a20d99b74d76f67/xxhash-4.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d", size = 511041, upload-time = "2026-08-17T08:20:36.771Z" }, + { url = "https://files.pythonhosted.org/packages/83/97/31bd8b8279e6935a0719f6910ced15e9d5a2cd554b253f6027ce1b5a1c2c/xxhash-4.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee", size = 261812, upload-time = "2026-08-17T08:22:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/d180a2da23c105d8e0b02d54f9f5841013fc81c233010ec781e31f1aee4c/xxhash-4.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88", size = 339878, upload-time = "2026-08-17T08:35:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3d/f584cd3172fe934f0f5a0a3917d0d7ce781f74d794fd43bb72be71c3ef6f/xxhash-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9", size = 272871, upload-time = "2026-08-17T08:20:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/2c7956b2b551682e00b9aebce9ceb0a991a131d65f9850c09f5f9760be2e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3", size = 301440, upload-time = "2026-08-17T08:21:35.911Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/0739f6482184a8026f4b022718f5f815d352059312e80696825433f0a8e7/xxhash-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329", size = 260157, upload-time = "2026-08-17T08:22:01.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b31a7bcf1d7d116842812e54f9b944843b4236ea4fa85634e8259f342212/xxhash-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592", size = 278233, upload-time = "2026-08-17T08:21:15.674Z" }, + { url = "https://files.pythonhosted.org/packages/db/e8/5293bae090fc6119dbc5fcf5c4cc0e1536394b52d73b7904d033836c73db/xxhash-4.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1", size = 330270, upload-time = "2026-08-17T08:20:51.128Z" }, + { url = "https://files.pythonhosted.org/packages/72/9e/e2ab12d40921f3f34c9317637d65e011aeababf8288356ea8d527de2c1d0/xxhash-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d", size = 478555, upload-time = "2026-08-17T08:22:04.183Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/c6148d39a49efa95f39b4cf0d41ef35a487f3b30f6fb1fc8fe8d8eab577e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377", size = 258174, upload-time = "2026-08-17T08:35:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fb/0b04b68d6c5bc71c7a2c344f1287327b67e607f28fbcfd937697caca64b6/xxhash-4.0.1-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b", size = 20767, upload-time = "2026-08-17T08:21:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/a6/be/476092aba34d1fcd313e1613a3bb3bc692f253d167b54bc90049043b5034/xxhash-4.0.1-cp312-cp312-win32.whl", hash = "sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433", size = 34669, upload-time = "2026-08-17T08:21:49.486Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/f9413d94fae43cec6d1a74c4f12156c6f4a7f5fd50e1d34defebdee3dec9/xxhash-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e", size = 37073, upload-time = "2026-08-17T08:22:04.886Z" }, + { url = "https://files.pythonhosted.org/packages/c1/83/6fe93c1b95acf962bc61a246df09dc2dcce895ccfc1080c9f48d0b652b92/xxhash-4.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a", size = 33299, upload-time = "2026-08-17T08:35:12.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/c707286b527722f776e1fb81dd202c45623355ba1a2972337a2a26075b2b/xxhash-4.0.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a", size = 43639, upload-time = "2026-08-17T08:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3b/bb71639a0f95635f61936a6f2653599c4261b645ddddd8d00f9dfe3613e2/xxhash-4.0.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256", size = 40657, upload-time = "2026-08-17T08:22:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/76f3f5385faa9886a36f21fcc603f40b4c0c40ce622382f133160c48b4d9/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d", size = 34708, upload-time = "2026-08-17T08:35:24.614Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4a/f48f0e3e1b1ab072979fff2a5be899234e28090883e8b519d0b10215d708/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d", size = 35650, upload-time = "2026-08-17T08:21:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/c4/53/b73d7472b196101ad1f57ed0674af3af803ac3e9ec2feadd650a7b262562/xxhash-4.0.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5", size = 37958, upload-time = "2026-08-17T08:22:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/024946ad8fa532074af4e4380179da54b7ec9facc8bd0b279ec0fac4e63a/xxhash-4.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf", size = 38032, upload-time = "2026-08-17T08:22:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/934af8d99bb5885711006bec30a691f728edd513d2c40f053f887d8e7577/xxhash-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276", size = 35895, upload-time = "2026-08-17T08:35:16.53Z" }, + { url = "https://files.pythonhosted.org/packages/20/5f/a8011f6a1558f7ca66d9077bb4f192b1871afcea62fbd5733605d2015755/xxhash-4.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794", size = 259464, upload-time = "2026-08-17T08:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/9665a44397547e7a3d58c0942425a976d58dcfd4b538f33220a312bf6912/xxhash-4.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd", size = 283949, upload-time = "2026-08-17T08:22:17.003Z" }, + { url = "https://files.pythonhosted.org/packages/34/2d/78774141266457468f29f3f5803092df4db87d8148ba74e4debd041649db/xxhash-4.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360", size = 303898, upload-time = "2026-08-17T08:35:27.951Z" }, + { url = "https://files.pythonhosted.org/packages/59/48/d78d22de576b42528bff87c14207de50de4f0b888221a50ff7c9d675d670/xxhash-4.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51", size = 287241, upload-time = "2026-08-17T08:21:13.042Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/7a1755a59c59fd46176f293bbdd99e399a6537ba9537fc723aa4d1bf6e27/xxhash-4.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68", size = 519856, upload-time = "2026-08-17T08:22:15.35Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fb/76580c08e916507859b0f335393cb5fdc59452c4402edbc6bcca6e47e7df/xxhash-4.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349", size = 268572, upload-time = "2026-08-17T08:22:14.533Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/1abde3e07b8f2077a38b4fbfaf764115008bfe0ff03bc7756a52c9fd0607/xxhash-4.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57", size = 344967, upload-time = "2026-08-17T08:35:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/80b6ddf0732eef48a8b5fe717398274794392bd6dbe82af38d189d214772/xxhash-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f", size = 279956, upload-time = "2026-08-17T08:21:24.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/11cbc43c205bf81fad50d69c7319cd1b1ccc01a66cd4fb8766357126c43d/xxhash-4.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa", size = 307583, upload-time = "2026-08-17T08:22:22.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/11/cf0bc07feb2791045b6ac075d4bf64f1a5beedef2f46ae70d7104d63a19f/xxhash-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91", size = 265848, upload-time = "2026-08-17T08:35:31.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/7ada4bea2a2795073dfc42d96842930efbe7a0c1857ef4b522e4e90e5d83/xxhash-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54", size = 284409, upload-time = "2026-08-17T08:21:23.156Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f4/d8ce83dd6b99ccfbdadaf2db968ae40334d2e5f73a0297e593b9ddb3df39/xxhash-4.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53", size = 335921, upload-time = "2026-08-17T08:22:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9f/f47d8724bd8bc45b395b06b7cacea2dae0d00031af1b707184a091161df6/xxhash-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d", size = 487023, upload-time = "2026-08-17T08:22:19.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/2d87098f3371cc1e42dd04d2285ad56bca4c56667bc501bff02d2b9fd6b5/xxhash-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb", size = 264333, upload-time = "2026-08-17T08:35:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/93795ca5898ec7d7d0455283ad261c0fc76b4f0c0a69e86233bd7badb0bd/xxhash-4.0.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670", size = 20581, upload-time = "2026-08-17T08:21:39.207Z" }, + { url = "https://files.pythonhosted.org/packages/b6/96/926f7335a0a1647952c00421e8da877f658094f61336306c7cadc335c94d/xxhash-4.0.1-cp313-cp313-win32.whl", hash = "sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788", size = 34449, upload-time = "2026-08-17T08:22:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/8a5aeb811de093bab3434e77eff0e9461624a1a56a6a93d315d080aab2aa/xxhash-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375", size = 36520, upload-time = "2026-08-17T08:35:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/14/97f3c74000ca36955e9cb86f6d270dcd5848b5c65afa623453f5cf2d83d6/xxhash-4.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a", size = 33428, upload-time = "2026-08-17T08:21:31.713Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/ea406a02b561d3275232ccfdb3e29df80f7a65414940e3a15721c7bea40f/xxhash-4.0.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df", size = 43747, upload-time = "2026-08-17T08:22:31.37Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/b0c94d61ccf6b5d1f8847b58ef8f923125ac4919ed5bd0eb082750ca7cbd/xxhash-4.0.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e", size = 40749, upload-time = "2026-08-17T08:22:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c5/8085881a538983be0fd1c865d5df236242fea496044e2c8ca32b9f2ba39c/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d", size = 34734, upload-time = "2026-08-17T08:35:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/8803d13c968fc75ca434eea991d29ac5fd8a36b4afc9a6a9803c53933db4/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e", size = 35671, upload-time = "2026-08-17T08:21:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ad91d7f0fd294190d37c08236fe661f5c4e3f83dcd1a121877a2e64681ce/xxhash-4.0.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269", size = 38094, upload-time = "2026-08-17T08:22:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2b7ebdc1869caca5f02c4cba8379b631050d3c3d4adb9187e4dc1a6b8d3c/xxhash-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3", size = 38244, upload-time = "2026-08-17T08:35:38.081Z" }, + { url = "https://files.pythonhosted.org/packages/90/9d/f66cf6935f528e575f1ae4d6560d376e7587569747186f4fae8777cadc1b/xxhash-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873", size = 35904, upload-time = "2026-08-17T08:21:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/34569d7b482f0dc060074faafd163c588f915cbc3e3e218f1ffd8a3ad340/xxhash-4.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9", size = 259595, upload-time = "2026-08-17T08:22:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/a2370acfcd48732cf5c2b87f06cfbf7fa51c0ce0dd736bde42939eb9ebf7/xxhash-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2", size = 284279, upload-time = "2026-08-17T08:22:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/15/17d33c24e6c4a1c0b9ddc5584f0c25d51d48b34bacde1416a2235a19db4b/xxhash-4.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6", size = 303973, upload-time = "2026-08-17T08:35:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e0/4ec0d69ad5738729098a61e631b7ed2df22a922b0e03014b597c72bd863d/xxhash-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f", size = 287535, upload-time = "2026-08-17T08:21:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4f9b17e7a9eb71c65548ecddd9c18b84e3c18ca41c4d436ad2a3000d3f7b/xxhash-4.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa", size = 519257, upload-time = "2026-08-17T08:22:43.272Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/3276b3e743b8ddbed9c3f71c76d9dd6a75d72aa4e678b1447b635cfd92e0/xxhash-4.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d", size = 268190, upload-time = "2026-08-17T08:35:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/f1555de3c96721320930dbb7988c8482d82b85970076aba1a8d40e83ad43/xxhash-4.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd", size = 345553, upload-time = "2026-08-17T08:21:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c28908f27007087b61139d290f908dd827ffd40b88af0c43f9e1a1a7ffd5/xxhash-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736", size = 280499, upload-time = "2026-08-17T08:22:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/3ef57622c65816348f8196273485baab4752aae064959901e85cd867e067/xxhash-4.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f", size = 307211, upload-time = "2026-08-17T08:22:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/5804504bbc808968e57d6a50286dd8f8cc06e0ddd6e4ab4b1dc89ae42f35/xxhash-4.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8", size = 265865, upload-time = "2026-08-17T08:35:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ee/8572fdfd70e7aaaf150af899871c2cc0bb88c3295ca82172a31e04ca5168/xxhash-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d", size = 284545, upload-time = "2026-08-17T08:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f8/6eadcca0904660c848b466524e82a233d16c9d2d5258433aaf3546142d86/xxhash-4.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6", size = 336022, upload-time = "2026-08-17T08:22:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/4aa107b81602d6d6d09ab5a607c530d2d3a6b28e2e9a59b01875bd877c54/xxhash-4.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7", size = 486671, upload-time = "2026-08-17T08:35:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/b7/b2bf9b5301e9cd5f2e335fea8da0f5cf209a6594cb1fe77754774ad4a6fd/xxhash-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc", size = 263744, upload-time = "2026-08-17T08:21:56.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/35b1c02177ae26234892c2310fb4822ba62411acccbf425ab8f9fd99354a/xxhash-4.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848", size = 20563, upload-time = "2026-08-17T08:35:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/a06300b165fbd6b0cb4a9742987f2e997a9f447ce3bf7c6ac97b862ce62a/xxhash-4.0.1-cp314-cp314-win32.whl", hash = "sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd", size = 35151, upload-time = "2026-08-17T08:22:44.283Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/c5b37296b78f80fc97124c0fee0c7bbd1bdb6f3b18bcd8748bb113b2d8fc/xxhash-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2", size = 37156, upload-time = "2026-08-17T08:35:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5e/248f9cd169c2fb62236bedfba246d213bce728f74901e99047e3f3c55875/xxhash-4.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056", size = 34379, upload-time = "2026-08-17T08:21:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/58/c8/db1d37c0da0324d0298f6abd931ca1d4736e049d9f2081230a8421da74d2/xxhash-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128", size = 38656, upload-time = "2026-08-17T08:22:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e18998ec465fb977bc74272e5bf3c2e886c13b014cbef916cd607802c709/xxhash-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f", size = 36306, upload-time = "2026-08-17T08:35:52.853Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1a/b83f86f8a987a3cbcb7e005a6824ff64aecae35abc1395a0d44ee16c3319/xxhash-4.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1", size = 273729, upload-time = "2026-08-17T08:21:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/2db15aa8508e0cd5b632927a53b98234f24039ea65377e6cf996c06d2d4f/xxhash-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc", size = 301083, upload-time = "2026-08-17T08:35:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/ed759787ffe802bd8e31cfcdad3755cbeca2dcdafd2f790cd6f25d195199/xxhash-4.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a", size = 312745, upload-time = "2026-08-17T08:22:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/f64b4a4cc8b51e950709207f55f7f56ae9c5af6631dd31d7fb443312418c/xxhash-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f", size = 301419, upload-time = "2026-08-17T08:35:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/bac313b8de073569b8db3152044a7cfcce87a3fa9698c18fe9f914dee6b1/xxhash-4.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e", size = 534485, upload-time = "2026-08-17T08:22:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/16b5e419f24e59507ee05626d2bb0deafdb03f9f27783bc0785a9849602e/xxhash-4.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb", size = 279605, upload-time = "2026-08-17T08:22:52.934Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/5787dd6e2d8d5b61256a5039f6b18c2193c7c1de4a2fd2413288d0d9c604/xxhash-4.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c", size = 358924, upload-time = "2026-08-17T08:35:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/89be41991f3b0a2e91f940bdf3128852c3ed571cf560d98ad0f67024afe4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c", size = 295305, upload-time = "2026-08-17T08:22:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5a/52ff0a0cc361aad393ff9a46ffe3aabbcf9c03d6c8f2612da7d553048276/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56", size = 320228, upload-time = "2026-08-17T08:35:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b5/91c60ff22c7f6cd5f6d7a5bad5a2cdcb4c33987dfa50bf13f0d856279b2e/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a", size = 279414, upload-time = "2026-08-17T08:23:03.212Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/9685954804d47d0390871a64bec606a0d536406382d71a784df3a5883fb4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914", size = 297594, upload-time = "2026-08-17T08:35:57.881Z" }, + { url = "https://files.pythonhosted.org/packages/89/62/b67ac9412907b7a07a2a0c08c3440b9e4480231a7b3de0767e87011e4564/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd", size = 348526, upload-time = "2026-08-17T08:22:18.571Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/6723cc49a9f567d52d01fd7c1741b0f2e3a13e71d15f7ac49d753a20c115/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f", size = 499307, upload-time = "2026-08-17T08:22:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/7b10e101ab988d93b791023be7191d7661271d6ab31ac082276b9091042a/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e", size = 274989, upload-time = "2026-08-17T08:36:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/7eabcc8d29cce40621443cff24c07d7306ef574b8956c47ac59f21098005/xxhash-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089", size = 35482, upload-time = "2026-08-17T08:22:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/2a4268e1971f63038b79fb75e3b9c8de942cd77acabbb0c5625352a31940/xxhash-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6", size = 37490, upload-time = "2026-08-17T08:35:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/7b/950ecab1fe4cf421d0a6211ddd9a0ac82e39e55c45a111ceb90953dc6c9a/xxhash-4.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f", size = 34596, upload-time = "2026-08-17T08:23:10.001Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/7dc3b85fac10751613bfedb0e120734e0e8710054abad3f931e9d3843a14/xxhash-4.0.1-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b", size = 43749, upload-time = "2026-08-17T08:36:00.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/bfac071c5b1c6d6a3d48ab1ab96a15e958a1d7061f4afc97804292d87264/xxhash-4.0.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d", size = 40758, upload-time = "2026-08-17T08:22:30.209Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/49a260e685d1a74c56a69432a8ee0527ddcbd684a3c51f87edc3b75639c5/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea", size = 34788, upload-time = "2026-08-17T08:23:09.014Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/50d72ed2170dae872e1c0fe333d0908e0a2afbffe74c5c9037d5406a4b89/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0", size = 35746, upload-time = "2026-08-17T08:36:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/66/f0/969deaa2bab3bfd5ad5b023442124d2255b9961eef6f797ec74eb8683bdf/xxhash-4.0.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c", size = 38098, upload-time = "2026-08-17T08:22:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/45ed7d7b8d7b66202a47bf8ff3b77cea28d2ea54dfcdd202b4cfe043e3dc/xxhash-4.0.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c", size = 38251, upload-time = "2026-08-17T08:35:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/45e7520a7856e13800a5dc8cd038d34c6372429465b163af0c5722f16918/xxhash-4.0.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34", size = 35986, upload-time = "2026-08-17T08:23:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/5ad466e5fea18c9f9bdc5828c0506f62190061b4a1b0e688aa54969d0a9e/xxhash-4.0.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706", size = 264609, upload-time = "2026-08-17T08:36:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/8f269f85217e3dbd45e31e25e46cc26f3aff0e159ef05d228b4b982c778c/xxhash-4.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6", size = 285910, upload-time = "2026-08-17T08:22:38.589Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/2fc1a16ee0f9501d074b798ebfae52e24fa602c7117f5c4b81de71eada72/xxhash-4.0.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59", size = 306566, upload-time = "2026-08-17T08:23:16.895Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/08375cf2b997e1903663fe7525c5973b1987a4f8ad2b8d47463e9143f2ee/xxhash-4.0.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b", size = 287978, upload-time = "2026-08-17T08:36:09.296Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/90a7b404c11add9e53a497d06236152852490c3b2f21e468d97a58f26afe/xxhash-4.0.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a", size = 520098, upload-time = "2026-08-17T08:22:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/11/02/7fba10b1b17eb46308f09cc0a4ed513d74dff16b1e22a1c439f011c77129/xxhash-4.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b", size = 268273, upload-time = "2026-08-17T08:35:29.043Z" }, + { url = "https://files.pythonhosted.org/packages/54/49/c21b228877357a3be43eeeaa22182ad1685796f415390ada475922c084e4/xxhash-4.0.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535", size = 350164, upload-time = "2026-08-17T08:23:29.494Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/c15bb4aa33d94b78a5553b52e7fa1070565f0199925aeadec3871de20ce9/xxhash-4.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824", size = 281975, upload-time = "2026-08-17T08:36:08.139Z" }, + { url = "https://files.pythonhosted.org/packages/18/7a/b1d0388315fe7752b7725b68a912667526a1dd48ed492fcc031ac03f4b52/xxhash-4.0.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a", size = 307872, upload-time = "2026-08-17T08:22:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a1/037cb2dd8cf725c9565dfe3712b2915c0e0276a9154913dbfcbcecbeb672/xxhash-4.0.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8", size = 268241, upload-time = "2026-08-17T08:23:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/67c44422d0ee082169b238ce24bd2796b82d7c21ed953471365df8c508d8/xxhash-4.0.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0", size = 284970, upload-time = "2026-08-17T08:36:13.476Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/254a5f51c4014cacc77a26f321372338b924f54e89efb730164ee336d850/xxhash-4.0.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9", size = 338074, upload-time = "2026-08-17T08:22:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/64/03/f21c4830118d72ef3a958ce8bf2152f49e0d4cf200907616c9be6caf372a/xxhash-4.0.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0", size = 487626, upload-time = "2026-08-17T08:35:32.768Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/268a689d741d7da649317eb4ce41760140beb4179aaf43a7216fdbe8100c/xxhash-4.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2", size = 263852, upload-time = "2026-08-17T08:23:41.871Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/adaf8101cd7f143191a0b390600294d83924b32cb13770fde8803dce27a2/xxhash-4.0.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9", size = 20569, upload-time = "2026-08-17T08:36:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/56a5eb8c993420fc07114c08f447a2b66ee996510b4764cb368b9b44c9f0/xxhash-4.0.1-cp315-cp315-win32.whl", hash = "sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330", size = 35145, upload-time = "2026-08-17T08:22:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/67/c7/65f210db43e62157d0fef3b4d4d7b394821e7733c8bb4ece49f91410a725/xxhash-4.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37", size = 37161, upload-time = "2026-08-17T08:23:32.438Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/1a641d1d60ba219756d9ebe907ff0ecf4445adcf4fa96f6e3da57b91d439/xxhash-4.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea", size = 34378, upload-time = "2026-08-17T08:36:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/7698b320b251806d1249e513922a626f19027e104c829a611272250350eb/xxhash-4.0.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f", size = 38610, upload-time = "2026-08-17T08:22:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/436497e775b647b3b3e9a4ffe8c76c59fa4aa7a9fab6447cb59acf1b50ea/xxhash-4.0.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0", size = 36378, upload-time = "2026-08-17T08:35:36.18Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/17a4f8182b9257898aa2a77c2a45f70233eb8e50681a280e8e09d2ee76e9/xxhash-4.0.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0", size = 273559, upload-time = "2026-08-17T08:23:51.075Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/121bd5a5c5adb88e0da772c7bef61964cf9da92956a7a237c7d24c4351b8/xxhash-4.0.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621", size = 299133, upload-time = "2026-08-17T08:36:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/57c7b6e04642ed738a0d08a31bed7fc63fdacb661d665f98739cc9751b62/xxhash-4.0.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5", size = 314527, upload-time = "2026-08-17T08:22:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/42793917dbab0ea1ff71458aea4875e17a7263f2797b798af048dc81e867/xxhash-4.0.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a", size = 299199, upload-time = "2026-08-17T08:23:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/51dc92443923d8e908d5614f1145d8d696450f9d6c8f1abe243c6f2a0222/xxhash-4.0.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619", size = 531967, upload-time = "2026-08-17T08:36:18.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/c5/d0de77de09661fac71742c4155b1cd65e274f7cc277819d702b6c8ff2db5/xxhash-4.0.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32", size = 278764, upload-time = "2026-08-17T08:22:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/9a/589929c655aba1bfb2c41ee03e50eec1547c39c3042a66bda9c173a9614b/xxhash-4.0.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60", size = 359876, upload-time = "2026-08-17T08:35:40.35Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a8/c1d8c94d54d91db2215565f4b4151c1593af3e6d27ac4c00fd1e8d714a02/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478", size = 293548, upload-time = "2026-08-17T08:23:54.951Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/85d8abca94508a4dd10561d9dea3e6e68843c6986dd6d9c1b3729c8622e4/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00", size = 318780, upload-time = "2026-08-17T08:36:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/1b/16/2b920ed456b9cdcfc99ddc20c3afe42f9f807ee5850773c12fd891f3c08d/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a", size = 277582, upload-time = "2026-08-17T08:22:57.156Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cc/5811b5997aebb8452047f5800d32fc50eaa29d0ba08d4e426f84450b9c2f/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579", size = 295116, upload-time = "2026-08-17T08:23:40.868Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/c2f3f9c2f4d6aadb79f17a9f1c9a7ee82638cc873680da044cf29537d2ee/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1", size = 347065, upload-time = "2026-08-17T08:36:21.618Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4c/750cc642c92252e10772ec09e1a1d995581ba4c3ceb24f6e2d57c7ce47ca/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3", size = 497208, upload-time = "2026-08-17T08:23:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2d/58693cb13d6395f39b6b9bb40c5e0db53a5df7c9fce805aa7e792f64a1a5/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513", size = 274338, upload-time = "2026-08-17T08:35:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/9aa9787586d9b3e92d63343ce7dc24f0f445fd9e74ff5d6e85dd82233df5/xxhash-4.0.1-cp315-cp315t-win32.whl", hash = "sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503", size = 35471, upload-time = "2026-08-17T08:24:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ab/4615789c333bee331ac417885c50105715eeb8244bfc68d2bc37dcfd63ca/xxhash-4.0.1-cp315-cp315t-win_amd64.whl", hash = "sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd", size = 37488, upload-time = "2026-08-17T08:36:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/49f718beb0c55d0411bc4bd90b50a3fbe5863a0e97a2f4d11682ba13d298/xxhash-4.0.1-cp315-cp315t-win_arm64.whl", hash = "sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875", size = 34590, upload-time = "2026-08-17T08:23:19.597Z" }, + { url = "https://files.pythonhosted.org/packages/86/79/9127ff42a887a348dc4ce3211cf1a962836887adee6f57078132bfba78b4/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80", size = 31836, upload-time = "2026-08-17T08:36:28.261Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/f238693bfdd642adb59c99683964d46d9947fe721ff44d3bd850ae675407/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b", size = 34453, upload-time = "2026-08-17T08:23:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/796ace33cdfb75c91ba6d11615c3bd436355b9f3103e05865bbee9abce57/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2", size = 38488, upload-time = "2026-08-17T08:23:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/2d549e5d5d7759eaf9ac2d2d2ab81ff60f1bb2b52cdaae8e5ec5c6524354/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f", size = 38206, upload-time = "2026-08-17T08:36:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/1ee576b27f78e6107ee4ea8ac03e8a52888dff256e57d560f8282c195563/xxhash-4.0.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626", size = 37127, upload-time = "2026-08-17T08:23:42.705Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4f/e0648288a17d0d1084ca4f7bef206097831988fc86af74aa1dff8f1fbd68/xxhash-4.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6", size = 36333, upload-time = "2026-08-17T08:23:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/34b7f72e9b5a8bfd7e6178de9e1e342bc3de9f07111a5ae26c00506d9edf/xxhash-4.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1", size = 33519, upload-time = "2026-08-17T08:36:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/4b/e0af324ccf701bf84a7a060bef11c915d45d9e3c5b9caf5b94d62ecb040b/xxhash-4.0.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee", size = 47995, upload-time = "2026-08-17T08:24:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/cc7130e6ca6b41ab72eb6a03177b933c7c74145545c99a8610ecc208c449/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468", size = 42725, upload-time = "2026-08-17T08:36:34.144Z" }, + { url = "https://files.pythonhosted.org/packages/1c/24/4f26ff9a7dd0998f6d1036bdddef7ce3e78972a74f7fffa7967e7bc3b7e4/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d", size = 39532, upload-time = "2026-08-17T08:23:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/1ac078fc8fceadcf066469acecacb35d2821cbfaf7d6fc5ac2107c7a314d/xxhash-4.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58", size = 37252, upload-time = "2026-08-17T08:24:06.652Z" }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, + { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, + { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, ] [[package]] From 4277132ff5fa3184a559861384e7488852a85d47 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:17 +0800 Subject: [PATCH 03/35] feat: add credential boundary, log redaction and resource guards Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/resources.py | 1924 ++++++++++++++++++++++++++++++++++++ src/utils/cleanup.py | 19 +- src/utils/log_manager.py | 35 +- src/utils/security.py | 448 +++++++++ 4 files changed, 2407 insertions(+), 19 deletions(-) create mode 100644 src/providers/resources.py create mode 100644 src/utils/security.py diff --git a/src/providers/resources.py b/src/providers/resources.py new file mode 100644 index 0000000..472d908 --- /dev/null +++ b/src/providers/resources.py @@ -0,0 +1,1924 @@ +"""显式的供应商资源能力包装。 + +这些 Facade 只负责把调用转发到已经初始化的 SDK 客户端,不把文件、批处理 +或缓存生命周期混入普通聊天请求。 +""" + +import inspect +from collections.abc import Callable +from functools import wraps +from typing import Any, TypeVar + +from src.utils.security import ( + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) + +_FacadeMethod = TypeVar("_FacadeMethod", bound=Callable[..., Any]) + + +_NATIVE_SCALARS = (str, bytes, bytearray, int, float, bool, complex, type(None)) +_RESOURCE_TREE_RETURNING_METHODS = frozenset({"with_options", "copy"}) + + +def _allow_resource_business_parameters(*names: str) -> Callable[[_FacadeMethod], _FacadeMethod]: + """声明官方 SDK 明确定义、但名称类似凭证的业务参数。""" + + allowed = frozenset(names) + + def decorator(method: _FacadeMethod) -> _FacadeMethod: + method._resource_business_parameters = allowed # type: ignore[attr-defined] + return method + + return decorator + + +class _NativeResourceProxy: + """为 SDK 原生资源树增加请求扩展校验,同时保持结果对象原样返回。 + + `.native` 仍然返回官方客户端本身;只有通过 Facade 动态访问的资源节点 + 使用此代理。这样既能兼容 SDK 新增资源,又不会让 `extra_headers`、 + `extra_query` 或 `extra_body` 绕过统一凭证边界。 + """ + + __slots__ = ("_children", "_path", "_provider", "_value") + + def __init__(self, value: Any, provider: Any, path: str): + self._value = value + self._provider = provider + self._path = path + self._children: dict[str, Any] = {} + + def __getattr__(self, name: str) -> Any: + if name in self._children: + return self._children[name] + value = getattr(self._value, name) + wrapped = _wrap_native_resource_value( + value, + self._provider, + f"{self._path}.{name}", + ) + self._children[name] = wrapped + return wrapped + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + safe_args = validate_secret_free_resource_args( + args, + f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", + ) + safe_kwargs = validate_secret_free_resource_kwargs( + kwargs, + f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", + ) + # Call results are deliberately not wrapped. They are response models, + # pagers, streams, or context managers rather than mutable resource + # trees; preserving their SDK identity keeps normal client code intact. + result = self._value(*safe_args, **safe_kwargs) + # ``with_options``/``copy`` return another SDK client (and some SDK + # resource nodes expose the same helpers). Keep that resource tree + # behind the proxy so subsequent calls cannot bypass the same guard. + method_name = self._path.rsplit(".", 1)[-1] + if method_name in _RESOURCE_TREE_RETURNING_METHODS: + return _wrap_native_resource_value(result, self._provider, self._path) + return result + + def __repr__(self) -> str: + return repr(self._value) + + +def _wrap_native_resource_value(value: Any, provider: Any, path: str) -> Any: + if isinstance(value, _NativeResourceProxy): + return value + if isinstance(value, _NATIVE_SCALARS): + return value + if isinstance(value, (dict, list, tuple, set, frozenset)): + return value + if inspect.ismodule(value) or inspect.isclass(value): + return value + return _NativeResourceProxy(value, provider, path) + + +def _resource_provider_label(provider: Any) -> str: + return str( + getattr(provider, "_provider", None) + or provider.__class__.__name__ + ) + + +def _guard_resource_method(method: _FacadeMethod) -> _FacadeMethod: + """在所有显式 Facade 方法前统一校验 SDK 扩展参数。""" + + allowed_business_parameters = frozenset( + getattr(method, "_resource_business_parameters", ()) + ) + + def sanitize_call( + self: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> tuple[tuple[Any, ...], dict[str, Any]]: + label = f"{_resource_provider_label(object.__getattribute__(self, 'provider'))} 资源" + if not allowed_business_parameters: + return ( + validate_secret_free_resource_args(args, label), + validate_secret_free_resource_kwargs(kwargs, label), + ) + + signature = inspect.signature(method) + bound = signature.bind_partial(self, *args, **kwargs) + for name, value in list(bound.arguments.items()): + if name == "self" or name in allowed_business_parameters: + continue + parameter = signature.parameters[name] + if parameter.kind is inspect.Parameter.VAR_POSITIONAL: + bound.arguments[name] = validate_secret_free_resource_args(value, label) + elif parameter.kind is inspect.Parameter.VAR_KEYWORD: + bound.arguments[name] = validate_secret_free_resource_kwargs(value, label) + else: + bound.arguments[name] = validate_secret_free_resource_kwargs( + {name: value}, label + )[name] + return bound.args[1:], dict(bound.kwargs) + + if inspect.iscoroutinefunction(method): + + @wraps(method) + async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + safe_args, safe_kwargs = sanitize_call(self, args, kwargs) + return await method(self, *safe_args, **safe_kwargs) + + return async_wrapper # type: ignore[return-value] + + @wraps(method) + def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + safe_args, safe_kwargs = sanitize_call(self, args, kwargs) + return method(self, *safe_args, **safe_kwargs) + + return wrapper # type: ignore[return-value] + + +class _NativeFacade: + """保留官方 SDK 的完整资源面,Facade 只负责稳定的常用别名。""" + + _delegate_native = False + _delegate_client_name = "sdk_client" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """为子类显式资源方法安装统一的请求扩展校验。""" + + super().__init_subclass__(**kwargs) + for name, method in list(cls.__dict__.items()): + if name.startswith("_") or not callable(method): + continue + setattr(cls, name, _guard_resource_method(method)) + + def __init__(self, provider: Any): + self.provider = provider + + @property + def native(self) -> Any: + return self.provider.sdk_client + + @property + def async_native(self) -> Any: + return self.provider.async_sdk_client + + def _delegated_resource(self, name: str, *, asynchronous: bool = False) -> Any: + """读取 SDK 资源并套用请求参数安全代理。 + + ``native``/``async_native`` 是有意保留的原生客户端出口;Facade 的 + 其它资源节点则必须经过同一个代理,避免显式属性绕过动态资源的 + ``extra_headers``、``extra_query`` 和 ``extra_body`` 校验。 + """ + client_name = "async_sdk_client" if asynchronous else "sdk_client" + client = getattr(self.provider, client_name) + try: + value = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"{_resource_provider_label(self.provider)} SDK 未提供 {name} 资源。" + ) from exc + if value is None: + raise NotImplementedError( + f"{_resource_provider_label(self.provider)} SDK 未提供 {name} 资源。" + ) + label = _resource_provider_label(self.provider) + return _wrap_native_resource_value(value, self.provider, f"{label}.{name}") + + @property + def beta(self) -> Any: + """访问供应商 SDK 的 beta 资源树(若该 SDK 提供)。""" + if not self._delegate_native: + raise AttributeError("beta") + return self._delegated_resource("beta") + + @property + def async_beta(self) -> Any: + """访问异步 SDK 的 beta 资源树(若该 SDK 提供)。""" + if not self._delegate_native: + raise AttributeError("async_beta") + return self._delegated_resource("beta", asynchronous=True) + + def __getattr__(self, name: str) -> Any: + """Expose the complete official SDK tree without widening compatible channels. + + Explicit facade methods remain the stable cross-provider surface. Official + providers additionally allow direct access to resources introduced by the + installed SDK after this module was released (for example Realtime, + Webhooks, Live, Interactions, or Beta resources). Dynamic nodes are + proxied so every callable still enforces the credential boundary; use + `.native` when an unwrapped SDK client is explicitly required. + """ + # Python introspection, copy/deepcopy and pickle probe dunder names via + # getattr. Never forward those probes to a third-party SDK resource. + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + if not self._delegate_native: + raise AttributeError(name) + client = getattr(self.provider, self._delegate_client_name) + try: + value = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise AttributeError(name) from exc + if value is None: + raise AttributeError(name) + return _wrap_native_resource_value( + value, + self.provider, + f"{_resource_provider_label(self.provider)}.{name}", + ) + + +class OpenAICompatibleResources(_NativeFacade): + """OpenAI-compatible channels expose only their native client by default.""" + + +class AsyncOpenAICompatibleResources(_NativeFacade): + """Async native-client access for OpenAI-compatible channels.""" + + +class _AsyncOnlyResourceFacade(_NativeFacade): + """Expose async-only SDK resources while retaining native delegation.""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + +class OpenAIResources(OpenAICompatibleResources): + """OpenAI 官方资源 Facade;兼容渠道不会继承此类能力声明。""" + + _delegate_native = True + + @property + def skills(self) -> Any: + """访问 OpenAI Skills 资源树。""" + return self._delegated_resource("skills") + + @property + def realtime(self) -> Any: + """访问 OpenAI Realtime 资源树。""" + return self._delegated_resource("realtime") + + @property + def webhooks(self) -> Any: + """访问 OpenAI Webhooks 资源树。""" + return self._delegated_resource("webhooks") + + @property + def admin(self) -> Any: + """访问 OpenAI Admin 资源树。""" + return self._delegated_resource("admin") + + @property + def content_provenance_checks(self) -> Any: + """访问 OpenAI 内容来源校验资源树。""" + return self._delegated_resource("content_provenance_checks") + + def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return self.provider.upload_file(file, purpose=purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def file_content(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.file_content(file_id, **kwargs) + + def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file_content(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.wait_for_file(file_id, **kwargs) + + def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + return self.provider.create_batch(input_file_id, endpoint=endpoint, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_batch(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_response(response_id, **kwargs) + + def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_response(response_id, **kwargs) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.delete_response(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.list_response_input_items(response_id, **kwargs) + + def connect_responses(self, **kwargs: Any) -> Any: + return self.provider.connect_responses(**kwargs) + + def stream_responses(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.stream_responses(request, **kwargs) + + def count_input_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_input_tokens(request, **kwargs) + + def create_vector_store(self, **kwargs: Any) -> Any: + return self.provider.create_vector_store(**kwargs) + + def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store(vector_store_id, **kwargs) + + def list_vector_stores(self, **kwargs: Any) -> Any: + return self.provider.list_vector_stores(**kwargs) + + def search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return self.provider.search_vector_store(vector_store_id, query, **kwargs) + + def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.delete_vector_store(vector_store_id, **kwargs) + + def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.update_vector_store(vector_store_id, **kwargs) + + def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file(vector_store_id, file_id, **kwargs) + + def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_and_poll(vector_store_id, file_id, **kwargs) + + def upload_vector_store_file(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file(vector_store_id, file, **kwargs) + + def upload_vector_store_file_and_poll(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file_and_poll(vector_store_id, file, **kwargs) + + def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store_file(vector_store_id, file_id, **kwargs) + + def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.list_vector_store_files(vector_store_id, **kwargs) + + def update_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.update_vector_store_file(vector_store_id, file_id, **kwargs) + + def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_vector_store_file(vector_store_id, file_id, **kwargs) + + def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.vector_store_file_content(vector_store_id, file_id, **kwargs) + + def poll_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.poll_vector_store_file(vector_store_id, file_id, **kwargs) + + def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_batch(vector_store_id, **kwargs) + + def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_batch_and_poll(vector_store_id, **kwargs) + + def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def poll_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.poll_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.list_vector_store_file_batch_files(vector_store_id, batch_id, **kwargs) + + def upload_vector_store_file_batch_and_poll(self, vector_store_id: str, files: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file_batch_and_poll(vector_store_id, files, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.retrieve_model(model, **kwargs) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.delete_model(model, **kwargs) + + def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return self.provider.create_moderation(input, **kwargs) + + def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return self.provider.generate_image(prompt, **kwargs) + + def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return self.provider.edit_image(image, prompt, **kwargs) + + def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return self.provider.create_image_variation(image, **kwargs) + + def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return self.provider.text_to_speech(text, model, voice, **kwargs) + + def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self.provider.transcribe_audio(file, model, **kwargs) + + def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self.provider.translate_audio(file, model, **kwargs) + + def create_video(self, **kwargs: Any) -> Any: + return self.provider.create_video(**kwargs) + + def create_video_and_poll(self, **kwargs: Any) -> Any: + return self.provider.create_video_and_poll(**kwargs) + + def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_video(video_id, **kwargs) + + def list_videos(self, **kwargs: Any) -> Any: + return self.provider.list_videos(**kwargs) + + def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.delete_video(video_id, **kwargs) + + def download_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.download_video(video_id, **kwargs) + + def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return self.provider.create_video_character(name, video, **kwargs) + + def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_video_character(character_id, **kwargs) + + def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return self.provider.edit_video(prompt, video, **kwargs) + + def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return self.provider.extend_video(prompt, seconds, video, **kwargs) + + def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return self.provider.remix_video(video_id, prompt, **kwargs) + + def poll_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.poll_video(video_id, **kwargs) + + def create_upload(self, **kwargs: Any) -> Any: + return self.provider.create_upload(**kwargs) + + def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self.provider.complete_upload(upload_id, **kwargs) + + def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_upload(upload_id, **kwargs) + + def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return self.provider.create_upload_part(upload_id, data, **kwargs) + + def upload_file_chunked(self, **kwargs: Any) -> Any: + return self.provider.upload_file_chunked(**kwargs) + + def parse(self, text_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.parse(text_format, request, **kwargs) + + def compact_responses(self, **kwargs: Any) -> Any: + return self.provider.compact_responses(**kwargs) + + def create_conversation(self, **kwargs: Any) -> Any: + return self.provider.create_conversation(**kwargs) + + def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_conversation(conversation_id, **kwargs) + + def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.update_conversation(conversation_id, **kwargs) + + def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.delete_conversation(conversation_id, **kwargs) + + def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.list_conversation_items(conversation_id, **kwargs) + + def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return self.provider.create_conversation_items(conversation_id, items, **kwargs) + + def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_conversation_item(conversation_id, item_id, **kwargs) + + def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self.provider.delete_conversation_item(conversation_id, item_id, **kwargs) + + def create_container(self, **kwargs: Any) -> Any: + return self.provider.create_container(**kwargs) + + def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_container(container_id, **kwargs) + + def list_containers(self, **kwargs: Any) -> Any: + return self.provider.list_containers(**kwargs) + + def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.delete_container(container_id, **kwargs) + + def create_container_file(self, container_id: str, file: Any = None, file_id: str | None = None, **kwargs: Any) -> Any: + return self.provider.create_container_file(container_id, file=file, file_id=file_id, **kwargs) + + def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.list_container_files(container_id, **kwargs) + + def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_container_file(container_id, file_id, **kwargs) + + def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_container_file(container_id, file_id, **kwargs) + + def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.container_file_content(container_id, file_id, **kwargs) + + def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return self.provider.create_fine_tuning_job(**kwargs) + + def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return self.provider.list_fine_tuning_jobs(**kwargs) + + def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_fine_tuning_job(job_id, **kwargs) + + def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_fine_tuning_job(job_id, **kwargs) + + def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.pause_fine_tuning_job(job_id, **kwargs) + + def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.resume_fine_tuning_job(job_id, **kwargs) + + def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.list_fine_tuning_events(job_id, **kwargs) + + def create_eval(self, **kwargs: Any) -> Any: + return self.provider.create_eval(**kwargs) + + def list_evals(self, **kwargs: Any) -> Any: + return self.provider.list_evals(**kwargs) + + def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_eval(eval_id, **kwargs) + + def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.update_eval(eval_id, **kwargs) + + def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.delete_eval(eval_id, **kwargs) + + def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.create_eval_run(eval_id, **kwargs) + + def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.list_eval_runs(eval_id, **kwargs) + + def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_eval_run(eval_id, run_id, **kwargs) + + def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_eval_run(eval_id, run_id, **kwargs) + + def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.delete_eval_run(eval_id, run_id, **kwargs) + + def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.list_eval_run_output_items(eval_id, run_id, **kwargs) + + def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return self.provider.retrieve_eval_run_output_item( + eval_id, run_id, output_item_id, **kwargs + ) + + +class AsyncOpenAIResources(AsyncOpenAICompatibleResources): + """OpenAI AsyncOpenAI 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + @property + def skills(self) -> Any: + """访问 AsyncOpenAI Skills 资源树。""" + return self._delegated_resource("skills", asynchronous=True) + + @property + def realtime(self) -> Any: + """访问 AsyncOpenAI Realtime 资源树。""" + return self._delegated_resource("realtime", asynchronous=True) + + @property + def webhooks(self) -> Any: + """访问 AsyncOpenAI Webhooks 资源树。""" + return self._delegated_resource("webhooks", asynchronous=True) + + @property + def admin(self) -> Any: + """访问 AsyncOpenAI Admin 资源树。""" + return self._delegated_resource("admin", asynchronous=True) + + @property + def content_provenance_checks(self) -> Any: + """访问 AsyncOpenAI 内容来源校验资源树。""" + return self._delegated_resource("content_provenance_checks", asynchronous=True) + + async def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, purpose=purpose, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def file_content(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_file_content(file_id, **kwargs) + + async def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file_content(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_wait_for_file(file_id, **kwargs) + + async def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(input_file_id, endpoint=endpoint, **kwargs) + + async def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_batch(batch_id, **kwargs) + + async def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(batch_id, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_response(response_id, **kwargs) + + async def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_response(response_id, **kwargs) + + async def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_response(response_id, **kwargs) + + async def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_response_input_items(response_id, **kwargs) + + def connect_responses(self, **kwargs: Any) -> Any: + return self.provider.async_connect_responses(**kwargs) + + def stream_responses(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_stream_responses(request, **kwargs) + + async def count_input_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_input_tokens(request, **kwargs) + + async def create_vector_store(self, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store(**kwargs) + + async def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store(vector_store_id, **kwargs) + + async def list_vector_stores(self, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_stores(**kwargs) + + async def search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return await self.provider.async_search_vector_store(vector_store_id, query, **kwargs) + + async def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_vector_store(vector_store_id, **kwargs) + + async def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_vector_store(vector_store_id, **kwargs) + + async def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file(vector_store_id, file_id, **kwargs) + + async def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_and_poll(vector_store_id, file_id, **kwargs) + + async def upload_vector_store_file(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file(vector_store_id, file, **kwargs) + + async def upload_vector_store_file_and_poll(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file_and_poll(vector_store_id, file, **kwargs) + + async def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_batch(vector_store_id, **kwargs) + + async def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_batch_and_poll(vector_store_id, **kwargs) + + async def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store_file(vector_store_id, file_id, **kwargs) + + async def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_store_files(vector_store_id, **kwargs) + + async def update_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_vector_store_file(vector_store_id, file_id, **kwargs) + + async def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_vector_store_file(vector_store_id, file_id, **kwargs) + + async def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_vector_store_file_content(vector_store_id, file_id, **kwargs) + + async def poll_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_vector_store_file(vector_store_id, file_id, **kwargs) + + async def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def poll_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_store_file_batch_files(vector_store_id, batch_id, **kwargs) + + async def upload_vector_store_file_batch_and_poll(self, vector_store_id: str, files: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file_batch_and_poll(vector_store_id, files, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_model(model, **kwargs) + + async def delete_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_model(model, **kwargs) + + async def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_moderation(input, **kwargs) + + async def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_generate_image(prompt, **kwargs) + + async def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_edit_image(image, prompt, **kwargs) + + async def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_image_variation(image, **kwargs) + + async def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return await self.provider.async_text_to_speech(text, model, voice, **kwargs) + + async def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self.provider.async_transcribe_audio(file, model, **kwargs) + + async def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self.provider.async_translate_audio(file, model, **kwargs) + + async def create_video(self, **kwargs: Any) -> Any: + return await self.provider.async_create_video(**kwargs) + + async def create_video_and_poll(self, **kwargs: Any) -> Any: + return await self.provider.async_create_video_and_poll(**kwargs) + + async def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_video(video_id, **kwargs) + + async def list_videos(self, **kwargs: Any) -> Any: + return await self.provider.async_list_videos(**kwargs) + + async def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_video(video_id, **kwargs) + + async def download_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_download_video(video_id, **kwargs) + + async def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_video_character(name, video, **kwargs) + + async def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_video_character(character_id, **kwargs) + + async def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_edit_video(prompt, video, **kwargs) + + async def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_extend_video(prompt, seconds, video, **kwargs) + + async def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_remix_video(video_id, prompt, **kwargs) + + async def poll_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_video(video_id, **kwargs) + + async def create_upload(self, **kwargs: Any) -> Any: + return await self.provider.async_create_upload(**kwargs) + + async def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self.provider.async_complete_upload(upload_id, **kwargs) + + async def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_upload(upload_id, **kwargs) + + async def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_upload_part(upload_id, data, **kwargs) + + async def upload_file_chunked(self, **kwargs: Any) -> Any: + return await self.provider.async_upload_file_chunked(**kwargs) + + async def parse(self, text_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_parse(text_format, request, **kwargs) + + async def compact_responses(self, **kwargs: Any) -> Any: + return await self.provider.async_compact_responses(**kwargs) + + async def create_conversation(self, **kwargs: Any) -> Any: + return await self.provider.async_create_conversation(**kwargs) + + async def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_conversation(conversation_id, **kwargs) + + async def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_conversation(conversation_id, **kwargs) + + async def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_conversation(conversation_id, **kwargs) + + async def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_conversation_items(conversation_id, **kwargs) + + async def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_conversation_items(conversation_id, items, **kwargs) + + async def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_conversation_item(conversation_id, item_id, **kwargs) + + async def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_conversation_item(conversation_id, item_id, **kwargs) + + async def create_container(self, **kwargs: Any) -> Any: + return await self.provider.async_create_container(**kwargs) + + async def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_container(container_id, **kwargs) + + async def list_containers(self, **kwargs: Any) -> Any: + return await self.provider.async_list_containers(**kwargs) + + async def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_container(container_id, **kwargs) + + async def create_container_file(self, container_id: str, file: Any = None, file_id: str | None = None, **kwargs: Any) -> Any: + return await self.provider.async_create_container_file(container_id, file=file, file_id=file_id, **kwargs) + + async def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_container_files(container_id, **kwargs) + + async def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_container_file(container_id, file_id, **kwargs) + + async def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_container_file(container_id, file_id, **kwargs) + + async def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_container_file_content(container_id, file_id, **kwargs) + + async def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return await self.provider.async_create_fine_tuning_job(**kwargs) + + async def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return await self.provider.async_list_fine_tuning_jobs(**kwargs) + + async def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_fine_tuning_job(job_id, **kwargs) + + async def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_fine_tuning_job(job_id, **kwargs) + + async def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_pause_fine_tuning_job(job_id, **kwargs) + + async def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_resume_fine_tuning_job(job_id, **kwargs) + + async def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_fine_tuning_events(job_id, **kwargs) + + async def create_eval(self, **kwargs: Any) -> Any: + return await self.provider.async_create_eval(**kwargs) + + async def list_evals(self, **kwargs: Any) -> Any: + return await self.provider.async_list_evals(**kwargs) + + async def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_eval(eval_id, **kwargs) + + async def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_eval(eval_id, **kwargs) + + async def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_eval(eval_id, **kwargs) + + async def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_eval_run(eval_id, **kwargs) + + async def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_eval_runs(eval_id, **kwargs) + + async def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_eval_run(eval_id, run_id, **kwargs) + + async def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_eval_run(eval_id, run_id, **kwargs) + + async def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_eval_run(eval_id, run_id, **kwargs) + + async def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_eval_run_output_items(eval_id, run_id, **kwargs) + + async def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return await self.provider.async_retrieve_eval_run_output_item( + eval_id, run_id, output_item_id, **kwargs + ) + + +class GoogleResources(_NativeFacade): + + _delegate_native = True + + @property + def auth_tokens(self) -> Any: + """访问 google-genai 的 API token 管理资源。""" + return self._delegated_resource("auth_tokens") + + def create_auth_token(self, **kwargs: Any) -> Any: + return self.provider.create_auth_token(**kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_file(file, **kwargs) + + @_allow_resource_business_parameters("auth") + def register_files(self, auth: Any, uris: list[str], **kwargs: Any) -> Any: + return self.provider.register_files(auth=auth, uris=uris, **kwargs) + + def compute_tokens(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.compute_tokens(request, **kwargs) + + def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_tokens(request, **kwargs) + + def get_file(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file(name, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def download_file(self, file: Any, **kwargs: Any) -> bytes: + return self.provider.download_file(file, **kwargs) + + def delete_file(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file(name, **kwargs) + + def create_cache(self, **kwargs: Any) -> Any: + return self.provider.create_cache(**kwargs) + + def get_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_cache(name, **kwargs) + + def list_caches(self, **kwargs: Any) -> Any: + return self.provider.list_caches(**kwargs) + + def update_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.update_cache(name, **kwargs) + + def delete_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_cache(name, **kwargs) + + def create_batch(self, src: Any, **kwargs: Any) -> Any: + return self.provider.create_batch(src, **kwargs) + + def create_embedding_batch(self, src: Any, **kwargs: Any) -> Any: + return self.provider.create_embedding_batch(src, **kwargs) + + def get_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_batch(name, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def cancel_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(name, **kwargs) + + def delete_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_batch(name, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def get_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.get_model(model, **kwargs) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.delete_model(model, **kwargs) + + def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self.provider.update_model(model, config=config, **kwargs) + + def tune( + self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any + ) -> Any: + return self.provider.tune( + base_model, training_dataset, config=config, **kwargs + ) + + def get_tuning(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_tuning(name, **kwargs) + + def list_tunings(self, **kwargs: Any) -> Any: + return self.provider.list_tunings(**kwargs) + + def cancel_tuning(self, name: str, **kwargs: Any) -> Any: + return self.provider.cancel_tuning(name, **kwargs) + + def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self.provider.validate_tuning_reward( + parent, + sample_response, + example, + single_reward_config=single_reward_config, + composite_reward_config=composite_reward_config, + config=config, + **kwargs, + ) + + def recontext_image(self, source: Any, **kwargs: Any) -> Any: + return self.provider.recontext_image(source, **kwargs) + + def generate_images(self, prompt: str, **kwargs: Any) -> Any: + return self.provider.generate_images(prompt, **kwargs) + + def edit_image(self, prompt: str, reference_images: list[Any], **kwargs: Any) -> Any: + return self.provider.edit_image(prompt, reference_images, **kwargs) + + def upscale_image(self, image: Any, upscale_factor: str, **kwargs: Any) -> Any: + return self.provider.upscale_image(image, upscale_factor, **kwargs) + + def segment_image(self, source: Any, **kwargs: Any) -> Any: + return self.provider.segment_image(source, **kwargs) + + def generate_videos(self, **kwargs: Any) -> Any: + return self.provider.generate_videos(**kwargs) + + def get_operation(self, operation: Any, **kwargs: Any) -> Any: + return self.provider.get_operation(operation, **kwargs) + + def list_file_search_documents(self, parent: str, **kwargs: Any) -> Any: + return self.provider.list_file_search_documents(parent, **kwargs) + + def get_file_search_document(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file_search_document(name, **kwargs) + + def delete_file_search_document(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file_search_document(name, **kwargs) + + def create_chat(self, **kwargs: Any) -> Any: + return self.provider.create_chat(**kwargs) + + def create_interaction(self, **kwargs: Any) -> Any: + return self.provider.create_interaction(**kwargs) + + def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.get_interaction(interaction_id, **kwargs) + + def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_interaction(interaction_id, **kwargs) + + def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.delete_interaction(interaction_id, **kwargs) + + def create_agent(self, **kwargs: Any) -> Any: + return self.provider.create_agent(**kwargs) + + def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + return self.provider.get_agent(agent_id, **kwargs) + + def list_agents(self, **kwargs: Any) -> Any: + return self.provider.list_agents(**kwargs) + + def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + return self.provider.delete_agent(agent_id, **kwargs) + + def create_webhook(self, **kwargs: Any) -> Any: + return self.provider.create_webhook(**kwargs) + + def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.get_webhook(webhook_id, **kwargs) + + def list_webhooks(self, **kwargs: Any) -> Any: + return self.provider.list_webhooks(**kwargs) + + def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.update_webhook(webhook_id, **kwargs) + + def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.delete_webhook(webhook_id, **kwargs) + + def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.ping_webhook(webhook_id, **kwargs) + + def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.rotate_webhook_signing_secret(webhook_id, **kwargs) + + def create_environment(self, **kwargs: Any) -> Any: + return self.provider.create_environment(**kwargs) + + def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + return self.provider.get_environment(environment_id, **kwargs) + + def list_environments(self, **kwargs: Any) -> Any: + return self.provider.list_environments(**kwargs) + + def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + return self.provider.delete_environment(environment_id, **kwargs) + + def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + return self.provider.get_environment_files(environment_id, path, **kwargs) + + def create_trigger(self, **kwargs: Any) -> Any: + return self.provider.create_trigger(**kwargs) + + def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.get_trigger(trigger_id, **kwargs) + + def list_triggers(self, **kwargs: Any) -> Any: + return self.provider.list_triggers(**kwargs) + + def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.update_trigger(trigger_id, **kwargs) + + def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.delete_trigger(trigger_id, **kwargs) + + def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.run_trigger(trigger_id, **kwargs) + + def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.list_trigger_executions(trigger_id, **kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + return self.provider.connect_live(model=model, config=config) + + def create_file_search_store(self, **kwargs: Any) -> Any: + return self.provider.create_file_search_store(**kwargs) + + def get_file_search_store(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file_search_store(name, **kwargs) + + def list_file_search_stores(self, **kwargs: Any) -> Any: + return self.provider.list_file_search_stores(**kwargs) + + def delete_file_search_store(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file_search_store(name, **kwargs) + + def import_file_to_file_search_store(self, file_search_store_name: str, file_name: str, **kwargs: Any) -> Any: + return self.provider.import_file_to_file_search_store(file_search_store_name, file_name, **kwargs) + + def upload_to_file_search_store(self, file_search_store_name: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_to_file_search_store(file_search_store_name, file, **kwargs) + + def download_file_search_media(self, media_id: str, **kwargs: Any) -> bytes: + return self.provider.download_file_search_media(media_id, **kwargs) + + +class AsyncGoogleResources(_NativeFacade): + """google-genai `Client.aio` 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + @property + def auth_tokens(self) -> Any: + """访问 google-genai 异步 API token 管理资源。""" + return self._delegated_resource("auth_tokens", asynchronous=True) + + async def create_auth_token(self, **kwargs: Any) -> Any: + return await self.provider.async_create_auth_token(**kwargs) + + async def upload_file(self, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, **kwargs) + + @_allow_resource_business_parameters("auth") + async def register_files(self, auth: Any, uris: list[str], **kwargs: Any) -> Any: + return await self.provider.async_register_files(auth=auth, uris=uris, **kwargs) + + async def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_tokens(request, **kwargs) + + async def compute_tokens(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_compute_tokens(request, **kwargs) + + async def get_file(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file(name, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def download_file(self, file: Any, **kwargs: Any) -> bytes: + return await self.provider.async_download_file(file, **kwargs) + + async def delete_file(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(name, **kwargs) + + async def create_interaction(self, **kwargs: Any) -> Any: + return await self.provider.async_create_interaction(**kwargs) + + async def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_interaction(interaction_id, **kwargs) + + async def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_interaction(interaction_id, **kwargs) + + async def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_interaction(interaction_id, **kwargs) + + async def create_agent(self, **kwargs: Any) -> Any: + return await self.provider.async_create_agent(**kwargs) + + async def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_agent(agent_id, **kwargs) + + async def list_agents(self, **kwargs: Any) -> Any: + return await self.provider.async_list_agents(**kwargs) + + async def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_agent(agent_id, **kwargs) + + async def create_webhook(self, **kwargs: Any) -> Any: + return await self.provider.async_create_webhook(**kwargs) + + async def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_webhook(webhook_id, **kwargs) + + async def list_webhooks(self, **kwargs: Any) -> Any: + return await self.provider.async_list_webhooks(**kwargs) + + async def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_webhook(webhook_id, **kwargs) + + async def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_webhook(webhook_id, **kwargs) + + async def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_ping_webhook(webhook_id, **kwargs) + + async def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_rotate_webhook_signing_secret(webhook_id, **kwargs) + + async def create_environment(self, **kwargs: Any) -> Any: + return await self.provider.async_create_environment(**kwargs) + + async def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_environment(environment_id, **kwargs) + + async def list_environments(self, **kwargs: Any) -> Any: + return await self.provider.async_list_environments(**kwargs) + + async def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_environment(environment_id, **kwargs) + + async def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + return await self.provider.async_get_environment_files(environment_id, path, **kwargs) + + async def create_trigger(self, **kwargs: Any) -> Any: + return await self.provider.async_create_trigger(**kwargs) + + async def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_trigger(trigger_id, **kwargs) + + async def list_triggers(self, **kwargs: Any) -> Any: + return await self.provider.async_list_triggers(**kwargs) + + async def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_trigger(trigger_id, **kwargs) + + async def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_trigger(trigger_id, **kwargs) + + async def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_run_trigger(trigger_id, **kwargs) + + async def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_trigger_executions(trigger_id, **kwargs) + + async def create_cache(self, **kwargs: Any) -> Any: + return await self.provider.async_create_cache(**kwargs) + + async def get_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_cache(name, **kwargs) + + async def list_caches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_caches(**kwargs) + + async def update_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_update_cache(name, **kwargs) + + async def delete_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_cache(name, **kwargs) + + async def create_batch(self, src: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(src, **kwargs) + + async def create_embedding_batch(self, src: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_embedding_batch(src, **kwargs) + + async def get_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_batch(name, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def cancel_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(name, **kwargs) + + async def delete_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_batch(name, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def get_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_get_model(model, **kwargs) + + async def delete_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_model(model, **kwargs) + + async def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_update_model(model, config=config, **kwargs) + + async def tune( + self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any + ) -> Any: + return await self.provider.async_tune( + base_model, training_dataset, config=config, **kwargs + ) + + async def get_tuning(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_tuning(name, **kwargs) + + async def list_tunings(self, **kwargs: Any) -> Any: + return await self.provider.async_list_tunings(**kwargs) + + async def cancel_tuning(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_tuning(name, **kwargs) + + async def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self.provider.async_validate_tuning_reward( + parent, + sample_response, + example, + single_reward_config=single_reward_config, + composite_reward_config=composite_reward_config, + config=config, + **kwargs, + ) + + async def recontext_image(self, source: Any, **kwargs: Any) -> Any: + return await self.provider.async_recontext_image(source, **kwargs) + + async def generate_images(self, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_generate_images(prompt, **kwargs) + + async def edit_image(self, prompt: str, reference_images: list[Any], **kwargs: Any) -> Any: + return await self.provider.async_edit_image(prompt, reference_images, **kwargs) + + async def upscale_image(self, image: Any, upscale_factor: str, **kwargs: Any) -> Any: + return await self.provider.async_upscale_image(image, upscale_factor, **kwargs) + + async def segment_image(self, source: Any, **kwargs: Any) -> Any: + return await self.provider.async_segment_image(source, **kwargs) + + async def generate_videos(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_videos(**kwargs) + + async def get_operation(self, operation: Any, **kwargs: Any) -> Any: + return await self.provider.async_get_operation(operation, **kwargs) + + async def list_file_search_documents(self, parent: str, **kwargs: Any) -> Any: + return await self.provider.async_list_file_search_documents(parent, **kwargs) + + async def get_file_search_document(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file_search_document(name, **kwargs) + + async def delete_file_search_document(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file_search_document(name, **kwargs) + + async def create_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_create_chat(**kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回异步 Live API 上下文管理器,调用方使用 `async with`。""" + return self.provider.async_connect_live(model=model, config=config) + + async def create_file_search_store(self, **kwargs: Any) -> Any: + return await self.provider.async_create_file_search_store(**kwargs) + + async def get_file_search_store(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file_search_store(name, **kwargs) + + async def list_file_search_stores(self, **kwargs: Any) -> Any: + return await self.provider.async_list_file_search_stores(**kwargs) + + async def delete_file_search_store(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file_search_store(name, **kwargs) + + async def import_file_to_file_search_store(self, file_search_store_name: str, file_name: str, **kwargs: Any) -> Any: + return await self.provider.async_import_file_to_file_search_store( + file_search_store_name, file_name, **kwargs + ) + + async def upload_to_file_search_store(self, file_search_store_name: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_to_file_search_store( + file_search_store_name, file, **kwargs + ) + + async def download_file_search_media(self, media_id: str, **kwargs: Any) -> bytes: + return await self.provider.async_download_file_search_media(media_id, **kwargs) + + +class AnthropicResources(_NativeFacade): + + _delegate_native = True + + def _beta_resource(self, name: str) -> Any: + """返回 Anthropic Beta 资源,保留 SDK 的完整方法树。""" + return getattr(self.beta, name) + + @property + def beta_agents(self) -> Any: + return self._beta_resource("agents") + + @property + def beta_deployments(self) -> Any: + return self._beta_resource("deployments") + + @property + def beta_deployment_runs(self) -> Any: + return self._beta_resource("deployment_runs") + + @property + def beta_dreams(self) -> Any: + return self._beta_resource("dreams") + + @property + def beta_environments(self) -> Any: + return self._beta_resource("environments") + + @property + def beta_files(self) -> Any: + return self._beta_resource("files") + + @property + def beta_memory_stores(self) -> Any: + return self._beta_resource("memory_stores") + + @property + def beta_models(self) -> Any: + return self._beta_resource("models") + + @property + def beta_sessions(self) -> Any: + return self._beta_resource("sessions") + + @property + def beta_skills(self) -> Any: + return self._beta_resource("skills") + + @property + def beta_tunnels(self) -> Any: + return self._beta_resource("tunnels") + + @property + def beta_user_profiles(self) -> Any: + return self._beta_resource("user_profiles") + + @property + def beta_vaults(self) -> Any: + return self._beta_resource("vaults") + + @property + def beta_webhooks(self) -> Any: + return self._beta_resource("webhooks") + + def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_tokens(request, **kwargs) + + def create_batch(self, requests: Any, **kwargs: Any) -> Any: + return self.provider.create_batch(requests, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_batch(batch_id, **kwargs) + + def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.batch_results(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.delete_batch(batch_id, **kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_file(file, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def download_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.download_file(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_model(model_id, **kwargs) + + def parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.parse(output_format, request, **kwargs) + + def beta_create(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_create(request, **kwargs) + + def beta_parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_parse(output_format, request, **kwargs) + + def beta_count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.beta_count_tokens(request, **kwargs) + + def beta_stream(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_stream(request, **kwargs) + + def beta_tool_runner(self, tools: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_tool_runner(tools, request, **kwargs) + + +class AsyncAnthropicResources(_NativeFacade): + """Anthropic AsyncAnthropic 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + def _beta_resource(self, name: str) -> Any: + """返回 AsyncAnthropic Beta 资源,保留 SDK 的完整方法树。""" + return getattr(self.async_beta, name) + + @property + def beta_agents(self) -> Any: + return self._beta_resource("agents") + + @property + def beta_deployments(self) -> Any: + return self._beta_resource("deployments") + + @property + def beta_deployment_runs(self) -> Any: + return self._beta_resource("deployment_runs") + + @property + def beta_dreams(self) -> Any: + return self._beta_resource("dreams") + + @property + def beta_environments(self) -> Any: + return self._beta_resource("environments") + + @property + def beta_files(self) -> Any: + return self._beta_resource("files") + + @property + def beta_memory_stores(self) -> Any: + return self._beta_resource("memory_stores") + + @property + def beta_models(self) -> Any: + return self._beta_resource("models") + + @property + def beta_sessions(self) -> Any: + return self._beta_resource("sessions") + + @property + def beta_skills(self) -> Any: + return self._beta_resource("skills") + + @property + def beta_tunnels(self) -> Any: + return self._beta_resource("tunnels") + + @property + def beta_user_profiles(self) -> Any: + return self._beta_resource("user_profiles") + + @property + def beta_vaults(self) -> Any: + return self._beta_resource("vaults") + + @property + def beta_webhooks(self) -> Any: + return self._beta_resource("webhooks") + + async def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_tokens(request, **kwargs) + + async def create_batch(self, requests: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(requests, **kwargs) + + async def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_batch(batch_id, **kwargs) + + async def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_batch_results(batch_id, **kwargs) + + async def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(batch_id, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_batch(batch_id, **kwargs) + + async def upload_file(self, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def download_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_download_file(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_model(model_id, **kwargs) + + async def parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_parse(output_format, request, **kwargs) + + async def beta_create(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_beta_create(request, **kwargs) + + async def beta_parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_beta_parse(output_format, request, **kwargs) + + async def beta_count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_beta_count_tokens(request, **kwargs) + + def beta_stream(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_beta_stream(request, **kwargs) + + def beta_tool_runner(self, tools: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_beta_tool_runner(tools, request, **kwargs) + + +class ArkResources(_NativeFacade): + + _delegate_native = True + + def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + return self.provider.upload_file(file, purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.wait_for_file(file_id, **kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_response(response_id, **kwargs) + + def create_response(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.create_response(request, **kwargs) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.delete_response(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.list_response_input_items(response_id, **kwargs) + + def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + """Ark SDK 顶层 input_items 资源的直观别名。""" + return self.provider.list_input_items(response_id, **kwargs) + + def create_batch_chat(self, **kwargs: Any) -> Any: + return self.provider.create_batch_chat(**kwargs) + + def generate_batch_chat(self, **kwargs: Any) -> Any: + return self.provider.generate_batch_chat(**kwargs) + + def create_batch_embedding(self, **kwargs: Any) -> Any: + return self.provider.create_batch_embedding(**kwargs) + + def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + return self.provider.create_batch_multimodal_embedding(**kwargs) + + def create_context(self, **kwargs: Any) -> Any: + return self.provider.create_context(**kwargs) + + def context_complete(self, **kwargs: Any) -> Any: + return self.provider.context_complete(**kwargs) + + def create_content_generation_task(self, **kwargs: Any) -> Any: + return self.provider.create_content_generation_task(**kwargs) + + def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return self.provider.get_content_generation_task(task_id, **kwargs) + + def list_content_generation_tasks(self, **kwargs: Any) -> Any: + return self.provider.list_content_generation_tasks(**kwargs) + + def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return self.provider.delete_content_generation_task(task_id, **kwargs) + + def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + return self.provider.count_tokens(text, **kwargs) + + def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + return self.provider.multimodal_embed(inputs, **kwargs) + + def generate_image(self, **kwargs: Any) -> Any: + return self.provider.generate_image(**kwargs) + + def beta_chat_parse(self, **kwargs: Any) -> Any: + return self.provider.beta_chat_parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + return self.provider.beta_chat_stream(**kwargs) + + def bot_chat(self, **kwargs: Any) -> Any: + return self.provider.bot_chat(**kwargs) + + def classify( + self, query: str, labels: list[str], model: str | None = None, **kwargs: Any + ) -> Any: + return self.provider.classify(query, labels, model=model, **kwargs) + + +class AsyncArkResources(_NativeFacade): + """Ark AsyncArk 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + async def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, purpose, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_wait_for_file(file_id, **kwargs) + + async def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_response(response_id, **kwargs) + + async def create_response(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_create_response(request, **kwargs) + + async def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_response(response_id, **kwargs) + + async def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_response_input_items(response_id, **kwargs) + + async def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_input_items(response_id, **kwargs) + + async def create_batch_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_chat(**kwargs) + + async def generate_batch_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_batch_chat(**kwargs) + + async def create_batch_embedding(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_embedding(**kwargs) + + async def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_multimodal_embedding(**kwargs) + + async def create_context(self, **kwargs: Any) -> Any: + return await self.provider.async_create_context(**kwargs) + + async def context_complete(self, **kwargs: Any) -> Any: + return await self.provider.async_context_complete(**kwargs) + + async def create_content_generation_task(self, **kwargs: Any) -> Any: + return await self.provider.async_create_content_generation_task(**kwargs) + + async def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_content_generation_task(task_id, **kwargs) + + async def list_content_generation_tasks(self, **kwargs: Any) -> Any: + return await self.provider.async_list_content_generation_tasks(**kwargs) + + async def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_content_generation_task(task_id, **kwargs) + + async def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + return await self.provider.async_count_tokens(text, **kwargs) + + async def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + return await self.provider.async_multimodal_embed(inputs, **kwargs) + + async def generate_image(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_image(**kwargs) + + async def beta_chat_parse(self, **kwargs: Any) -> Any: + return await self.provider.async_beta_chat_parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + """返回可直接用于 ``async with`` 的 Ark 异步流上下文管理器。""" + return self.provider.async_beta_chat_stream(**kwargs) + + async def bot_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_bot_chat(**kwargs) + + async def classify( + self, query: str, labels: list[str], model: str | None = None, **kwargs: Any + ) -> Any: + return await self.provider.async_classify(query, labels, model=model, **kwargs) diff --git a/src/utils/cleanup.py b/src/utils/cleanup.py index e132118..60a27e3 100644 --- a/src/utils/cleanup.py +++ b/src/utils/cleanup.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -import os -import glob import atexit +import os import shutil -from .config import get_settings # 导入 get_settings 函数 -from .log_manager import get_module_logger # 导入日志管理器 + +from .config import get_settings # 导入 get_settings 函数 +from .log_manager import get_module_logger # 导入日志管理器 +from .security import redact_sensitive_text logger = get_module_logger(__name__) # 获取当前模块的日志器 @@ -23,9 +23,14 @@ def cleanup_temp_files(): shutil.rmtree(cache_dir) logger.info(f"已成功删除缓存目录: {cache_dir}") except OSError as e: - logger.error(f"删除缓存目录 {cache_dir} 时出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "删除缓存目录 %s 时出错: %s", + cache_dir, + error_text, + ) else: logger.info("未找到 .cache 目录,无需清理。") # 注册函数,使其在程序正常退出时被调用 -atexit.register(cleanup_temp_files) \ No newline at end of file +atexit.register(cleanup_temp_files) diff --git a/src/utils/log_manager.py b/src/utils/log_manager.py index 91330c2..831ffd7 100644 --- a/src/utils/log_manager.py +++ b/src/utils/log_manager.py @@ -1,11 +1,18 @@ -# -*- coding: utf-8 -*- import logging import os -import re # 导入 re 模块 +import re # 导入 re 模块 from datetime import datetime from logging.handlers import RotatingFileHandler from src.utils.config import get_settings +from src.utils.security import redact_sensitive_text + + +class RedactingFormatter(logging.Formatter): + """在格式化完整 traceback 后脱敏,避免异常链泄漏凭证。""" + + def format(self, record: logging.LogRecord) -> str: + return redact_sensitive_text(super().format(record)) def get_chat_logger() -> logging.Logger: """ @@ -25,12 +32,12 @@ def get_chat_logger() -> logging.Logger: # 控制台处理器 console_handler = logging.StreamHandler() - console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_formatter = RedactingFormatter('%(asctime)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # 文件处理器 (每天一个文件,最大 1MB,保留 5 个文件) - log_file_name = f"chat_log_{datetime.now().strftime('%Y-%m-%d')}.log" + log_file_name = f"chat_log_{datetime.now().astimezone().strftime('%Y-%m-%d')}.log" file_path = os.path.join(log_dir, log_file_name) file_handler = RotatingFileHandler( file_path, @@ -38,7 +45,7 @@ def get_chat_logger() -> logging.Logger: backupCount=5, encoding='utf-8' ) - file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) @@ -62,12 +69,12 @@ def get_module_logger(name: str) -> logging.Logger: # 控制台处理器 console_handler = logging.StreamHandler() - console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # 文件处理器 (每天一个文件,最大 1MB,保留 5 个文件) - log_file_name = f"app_log_{datetime.now().strftime('%Y-%m-%d')}.log" + log_file_name = f"app_log_{datetime.now().astimezone().strftime('%Y-%m-%d')}.log" file_path = os.path.join(log_dir, log_file_name) file_handler = RotatingFileHandler( file_path, @@ -75,7 +82,7 @@ def get_module_logger(name: str) -> logging.Logger: backupCount=5, encoding='utf-8' ) - file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') + file_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) @@ -92,7 +99,7 @@ def cleanup_old_logs(): if not os.path.exists(log_dir): return - now = datetime.now() + now = datetime.now().astimezone() # 获取所有日志文件 log_files = [f for f in os.listdir(log_dir) if f.endswith(".log")] @@ -104,7 +111,9 @@ def cleanup_old_logs(): match = re.search(r'(\d{4}-\d{2}-\d{2})', filename) if match: file_date_str = match.group(1) - file_date = datetime.strptime(file_date_str, '%Y-%m-%d') + file_date = datetime.strptime(file_date_str, '%Y-%m-%d').replace( + tzinfo=now.tzinfo + ) if (now - file_date).days > log_retention_days: os.remove(file_path) @@ -112,8 +121,10 @@ def cleanup_old_logs(): else: # 如果文件名不符合日期模式,也记录一下,但不删除 get_module_logger(__name__).warning(f"日志文件名不符合日期模式,跳过清理: {filename}") - except Exception as e: - get_module_logger(__name__).error(f"清理日志文件 {filename} 时出错: {e}", exc_info=True) + except Exception: # noqa: BLE001 - cleanup continues independently per file + get_module_logger(__name__).exception( + "清理日志文件 %s 时出错", filename + ) # 示例用法 (可选,用于测试) if __name__ == "__main__": diff --git a/src/utils/security.py b/src/utils/security.py new file mode 100644 index 0000000..a7606c3 --- /dev/null +++ b/src/utils/security.py @@ -0,0 +1,448 @@ +"""配置、请求和日志边界的安全校验工具。""" + +import copy +import re +from collections.abc import Collection, Mapping +from itertools import pairwise +from typing import Any +from urllib.parse import parse_qsl, urlsplit + +FORBIDDEN_OPTION_CONTAINERS = frozenset( + { + "headers", + "defaultheaders", + "setdefaultheaders", + "extraheaders", + "header", + "requestheaders", + "query", + "queryparams", + "params", + "searchparams", + "urlparams", + "requestquery", + "defaultquery", + "setdefaultquery", + "extraquery", + "httpoptions", + } +) + +SENSITIVE_OPTION_KEYS = frozenset( + { + "apikey", + "xapikey", + "accesskey", + "secretkey", + "token", + "password", + "passwd", + "authorization", + "auth", + "bearer", + "accesstoken", + "xaccesstoken", + "authtoken", + "xauthtoken", + "authkey", + "credential", + "credentials", + "cookie", + "setcookie", + "proxyauthorization", + "secret", + "privatekey", + "sshkey", + "sshprivatekey", + "signingkey", + "encryptionkey", + "googkey", + "googlekey", + "clientsecret", + "secretvalue", + "secretcredential", + "adminapikey", + "webhooksecret", + "workloadidentity", + "httpclient", + "httpxclient", + "httpxasyncclient", + "aiohttpclient", + "clientargs", + "asyncclientargs", + "baseurl", + "websocketbaseurl", + } +) + +_RESOURCE_EXTENSION_KEYS = frozenset({"extraheaders", "extraquery", "extrabody"}) +# These names are ordinary business parameters for some official SDK resource +# methods (for example ``vector_stores.search(query=...)``). They are safe only +# at the resource-argument container itself; nested keys and values remain +# subject to credential scanning. +_SAFE_RESOURCE_PARAMETER_CONTAINERS = frozenset({"query", "params"}) + +_BEARER_TEXT_RE = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+") +# A punctuation separator is unambiguous. Whitespace-only forms are useful +# for redacting messages such as ``token sk-...``, but treating every following +# word as a credential causes ordinary text (``token usage``) to be rejected by +# ``find_sensitive_option_paths``. Require a credential-like value for that +# form: at least twelve token characters and one digit or token punctuation. +_KEY_VALUE_TEXT_RE = re.compile( + r'''(?i)(api[_-]?key|access[_-]?key|secret[_-]?key|authorization|token|password)["']?''' + r'''(\s*[:=]\s*|\s+(?=["']|''' + r'''(?=[A-Za-z0-9._~+/=-]{12,}(?:[\s,;}']|$))[A-Za-z0-9._~+/=-]*[0-9._~+/=-]))''' + r'''(?:"[^"]*"|'[^']*'|[^\s,;}']+)''' +) +_URL_USERINFO_RE = re.compile(r"(?i)(https?://)([^\s/@:]+):([^\s/@]+)@") +_URL_QUERY_SECRET_RE = re.compile( + r"(?i)([?&](?:api[_-]?key|access[_-]?key|secret[_-]?key|token|password|authorization)=)" + r"[^&#\s]+" +) +_OPENAI_KEY_RE = re.compile(r"\b(?:sk|rk|sess)-[A-Za-z0-9_-]{8,}\b", re.IGNORECASE) +_GOOGLE_API_KEY_RE = re.compile(r"\bAIza[0-9A-Za-z_-]{20,}\b") + + +def redact_sensitive_text(value: Any) -> str: + """脱敏异常、日志和请求错误中的常见凭证表示。""" + if not isinstance(value, str): + value = str(value) + redacted = _BEARER_TEXT_RE.sub("Bearer [REDACTED]", value) + redacted = _KEY_VALUE_TEXT_RE.sub(r"\1=[REDACTED]", redacted) + redacted = _URL_USERINFO_RE.sub(r"\1[REDACTED]:[REDACTED]@", redacted) + redacted = _URL_QUERY_SECRET_RE.sub(r"\1[REDACTED]", redacted) + redacted = _OPENAI_KEY_RE.sub("[REDACTED]", redacted) + return _GOOGLE_API_KEY_RE.sub("[REDACTED]", redacted) + + +def _copy_nested(value: Any) -> Any: + """递归复制配置容器,尽量隔离可变的第三方 SDK 对象。""" + if isinstance(value, Mapping): + return {key: _copy_nested(nested) for key, nested in value.items()} + if isinstance(value, list): + return [_copy_nested(nested) for nested in value] + if isinstance(value, tuple): + return tuple(_copy_nested(nested) for nested in value) + if isinstance(value, set): + return {_copy_nested(nested) for nested in value} + if isinstance(value, frozenset): + return frozenset(_copy_nested(nested) for nested in value) + model_copy = getattr(value, "model_copy", None) + if callable(model_copy): + try: + copied = model_copy(deep=True) + except Exception: # noqa: BLE001 - SDK model copy is best effort + copied = value + if copied is not value: + return copied + try: + return copy.deepcopy(value) + except Exception: # noqa: BLE001 - preserve opaque runtime handles + return value + + +def _model_dump_mapping(value: Any) -> Mapping[str, Any] | None: + """读取第三方 SDK 模型的公开字典表示(若可用)。 + + Pydantic 型 SDK 配置对象(例如 ``google.genai.types.HttpOptions``)不是 + ``Mapping``,只遍历普通字典会让其中的 headers/base URL 绕过配置边界。 + 仅使用 SDK 明确提供的 ``model_dump``,无法读取时保持原有兼容行为。 + """ + model_dump = getattr(value, "model_dump", None) + if not callable(model_dump): + return None + try: + dumped = model_dump(exclude_none=True) + except TypeError: + try: + dumped = model_dump() + except Exception: # noqa: BLE001 - third-party model_dump must fail closed + return None + except Exception: # noqa: BLE001 - third-party model_dump must fail closed + return None + if dumped is value or not isinstance(dumped, Mapping): + return None + return dumped + + +def normalize_option_key(key: Any) -> str: + """将配置键规范化为大小写无关、分隔符无关的形式。""" + return "".join(char for char in str(key).lower() if char.isalnum()) + + +def is_sensitive_option_key(key: Any) -> bool: + """识别凭证键,避免把普通单词的 ``secret`` 子串误判为凭证。""" + normalized = normalize_option_key(key) + if normalized in FORBIDDEN_OPTION_CONTAINERS or normalized in SENSITIVE_OPTION_KEYS: + return True + + # 只对明确的词边界进行组合键识别;例如 ``secretary`` 不会命中, + # 而 ``client_secret``、``api-token`` 等显式凭证命名会命中。 + words = [ + part.lower() + for part in re.split( + r"[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", + str(key), + ) + if part + ] + if any( + word in { + "apikey", "accesstoken", "authtoken", "apitoken", "bearer", + "password", "cookie", "token", + } + for word in words + ): + return True + if any(word == "secret" for word in words): + return True + if any( + left in { + "api", "access", "auth", "client", "secret", "private", "ssh", + "signing", "encryption", "goog", "google", + } + and right in {"key", "token", "secret", "credential"} + for left, right in pairwise(words) + ): + return True + compact = "".join(words) + return compact in SENSITIVE_OPTION_KEYS or compact in FORBIDDEN_OPTION_CONTAINERS + + +def _url_credential_paths(value: Any, path: str) -> list[str]: + if not isinstance(value, str): + return [] + try: + parsed = urlsplit(value) + except ValueError: + return [] + found: list[str] = [] + if redact_sensitive_text(value) != value: + found.append(path or "value") + if parsed.username is not None or parsed.password is not None: + found.append(f"{path} userinfo") + if parsed.query and (parsed.scheme or parsed.netloc or "://" in value): + for key, _ in parse_qsl(parsed.query, keep_blank_values=True): + if is_sensitive_option_key(key): + found.append(f"{path}?{key}") + return found + + +def find_sensitive_option_paths( + options: Mapping[str, Any], + *, + allowed_containers: Collection[str] = (), +) -> list[str]: + """递归查找凭证、连接覆盖和 URL 中的凭证路径。 + + ``allowed_containers`` 只用于供应商明确支持的安全容器(例如 Google + ``http_options.headers``);容器内的键和值仍会继续递归检查。 + """ + found: list[str] = [] + allowed = {normalize_option_key(value) for value in allowed_containers} + + def visit(value: Any, path: str = "") -> None: + if isinstance(value, Mapping): + for key, nested in value.items(): + current = f"{path}.{key}" if path else str(key) + if is_sensitive_option_key(key) and normalize_option_key(key) not in allowed: + found.append(current) + continue + visit(nested, current) + return + dumped = _model_dump_mapping(value) + if dumped is not None: + visit(dumped, path) + return + if isinstance(value, (list, tuple, set, frozenset)): + for index, nested in enumerate(value): + visit(nested, f"{path}[{index}]") + return + found.extend(_url_credential_paths(value, path)) + + visit(options) + return sorted(set(found)) + + +def validate_secret_free_options( + options: Mapping[str, Any] | None, + provider: str, + *, + allowed_containers: Collection[str] = (), +) -> dict[str, Any]: + """校验模型级 options 不会携带凭证或请求边界覆盖。 + + 允许的容器只放宽容器本身,绝不会跳过其子项的敏感键检查。 + """ + if options is None: + return {} + if not isinstance(options, Mapping): + raise ValueError(f"{provider} options 必须是对象。") + found = find_sensitive_option_paths(options, allowed_containers=allowed_containers) + if found: + raise ValueError( + f"{provider} options 不允许包含凭证或请求头/query 配置: " + + ", ".join(found) + ) + # 不能只复制最外层:模型配置通常包含 ``extra_body``、嵌套路由或 + # SDK 配置对象。递归复制可以防止调用方在 Provider 初始化后修改 + # 原始字典,进而改变后续请求。 + return _copy_nested(dict(options)) + + +def validate_secret_free_request_overrides( + extra_headers: Mapping[str, Any] | None = None, + extra_query: Mapping[str, Any] | None = None, + provider: str = "Provider", +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """校验请求级 HTTP 覆盖项,不允许携带凭证。 + + 请求级扩展仍可用于追踪头、租户标识等非敏感字段;认证由 Provider + 自己管理,避免调用方通过 ``extra_headers``/``extra_query`` 绕过密钥边界。 + """ + + normalized: dict[str, Mapping[str, Any] | None] = { + "extra_headers": extra_headers, + "extra_query": extra_query, + } + copies: dict[str, dict[str, Any] | None] = {} + for field_name, value in normalized.items(): + if value is None: + copies[field_name] = None + continue + if not isinstance(value, Mapping): + raise ValueError(f"{provider} {field_name} 必须是对象。") + found = find_sensitive_option_paths(value) + if found: + raise ValueError( + f"{provider} 请求头/query 不允许包含凭证: " + f"{field_name}." + ", ".join(found) + ) + copies[field_name] = _copy_nested(dict(value)) + return copies["extra_headers"], copies["extra_query"] + + +def validate_secret_free_payload( + value: Mapping[str, Any] | None, + provider: str, + field_name: str, +) -> dict[str, Any]: + """校验可扩展请求体中的敏感字段并返回递归隔离的副本。""" + + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{provider} {field_name} 必须是对象。") + found = find_sensitive_option_paths(value) + if found: + raise ValueError( + f"{provider} {field_name} 不允许包含凭证或连接字段: " + + ", ".join(found) + ) + return _copy_nested(dict(value)) + + +def validate_secret_free_resource_kwargs( + kwargs: Mapping[str, Any] | None, + provider: str, +) -> dict[str, Any]: + """校验并复制资源 Facade 的 HTTP/body 扩展参数。 + + SDK 资源方法普遍接受 ``extra_headers``、``extra_query`` 和 + ``extra_body``。统一在 Facade 边界校验,避免某个资源方法遗漏安全检查。 + """ + + if kwargs is None: + return {} + if not isinstance(kwargs, Mapping): + raise ValueError(f"{provider} 资源参数必须是对象。") + sanitized = _copy_nested(dict(kwargs)) + + # SDK client/resource methods also expose client-level overrides such as + # ``api_key``, ``default_headers`` and ``base_url``. They are not part of + # the three supported request extensions below and must not pass through a + # Facade just because a particular SDK accepts them. + generic_kwargs = { + key: value + for key, value in sanitized.items() + if normalize_option_key(key) not in _RESOURCE_EXTENSION_KEYS + } + found = find_sensitive_option_paths( + generic_kwargs, + allowed_containers=_SAFE_RESOURCE_PARAMETER_CONTAINERS, + ) + if found: + raise ValueError( + f"{provider} 资源参数不允许包含凭证或请求头/query 配置: " + + ", ".join(found) + ) + + normalized_keys: dict[str, Any] = {} + duplicate_extension_keys: list[str] = [] + for key in sanitized: + normalized = normalize_option_key(key) + if normalized not in _RESOURCE_EXTENSION_KEYS: + continue + if normalized in normalized_keys: + duplicate_extension_keys.append(str(key)) + continue + normalized_keys[normalized] = key + if duplicate_extension_keys: + raise ValueError( + f"{provider} 资源参数包含重复的扩展字段: " + + ", ".join(duplicate_extension_keys) + ) + headers_key = normalized_keys.get("extraheaders") + query_key = normalized_keys.get("extraquery") + body_key = normalized_keys.get("extrabody") + headers, query = validate_secret_free_request_overrides( + sanitized.get(headers_key) if headers_key is not None else None, + sanitized.get(query_key) if query_key is not None else None, + provider, + ) + if headers_key is not None: + sanitized[headers_key] = headers + if query_key is not None: + sanitized[query_key] = query + if body_key is not None and sanitized[body_key] is not None: + sanitized[body_key] = validate_secret_free_payload( + sanitized[body_key], provider, "extra_body" + ) + return sanitized + + +def validate_secret_free_resource_args( + args: tuple[Any, ...] | None, + provider: str, +) -> tuple[Any, ...]: + """校验并复制资源 Facade 的位置参数。 + + 大多数 SDK 资源方法把业务参数声明为关键字参数,但少数入口(例如 + OpenAI Responses WebSocket 的 ``connect``)允许用位置参数传入 + ``extra_query``、``extra_headers`` 或连接选项。统一扫描位置参数可以 + 防止这些入口绕过关键字参数的凭证边界,同时保留普通字符串、ID 和 + 文件对象的转发兼容性。 + """ + + if args is None: + return () + if not isinstance(args, tuple): + raise ValueError(f"{provider} 资源位置参数必须是元组。") + sanitized = _copy_nested(args) + found: list[str] = [] + for index, value in enumerate(sanitized): + # Wrapping the value in a mapping lets the existing recursive scanner + # retain useful paths (for example ``args[0].Authorization``) and the + # same safe business containers used by keyword resource parameters. + found.extend( + find_sensitive_option_paths( + {f"args[{index}]": value}, + allowed_containers=_SAFE_RESOURCE_PARAMETER_CONTAINERS, + ) + ) + if found: + raise ValueError( + f"{provider} 资源位置参数不允许包含凭证或请求头/query 配置: " + + ", ".join(sorted(set(found))) + ) + return sanitized From 2cb7da6e38b02bf2545e452e2af970ab00542d34 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:18 +0800 Subject: [PATCH 04/35] feat: unify provider factory and abstract request model Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/__base__/model_provider.py | 1654 +++++++++++++++++++++- src/providers/factory.py | 579 +++++++- src/utils/config.py | 233 ++- 3 files changed, 2355 insertions(+), 111 deletions(-) diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 58246ac..6a5a291 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -1,56 +1,1676 @@ -# -*- coding: utf-8 -*- +import asyncio +import base64 +import binascii +import inspect +import json +import math +import struct from abc import ABC, abstractmethod -from typing import Any, AsyncGenerator, Dict, Generator, List, Tuple +from collections.abc import ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Generator, + Iterable, + Mapping, + Sequence, +) +from contextlib import asynccontextmanager, contextmanager +from dataclasses import dataclass, replace +from dataclasses import field as dataclass_field +from functools import wraps +from typing import Any, TypeVar + +from tenacity import ( + AsyncRetrying, + Retrying, + retry_if_exception, + stop_after_attempt, + wait_exponential, +) + +from src.utils.security import ( + validate_secret_free_options as _validate_secret_free_options, +) +from src.utils.security import ( + validate_secret_free_payload as _validate_secret_free_payload, +) +from src.utils.security import ( + validate_secret_free_request_overrides as _validate_secret_free_request_overrides, +) +from src.utils.security import ( + validate_secret_free_resource_args as _validate_secret_free_resource_args, +) +from src.utils.security import ( + validate_secret_free_resource_kwargs as _validate_secret_free_resource_kwargs, +) + +_UNSET_TEMPERATURE = object() + + +@dataclass +class CompletionRequest: + """统一聊天请求。 + + `prompt` 和 `system_prompt` 保留旧版便捷调用;需要多轮或多模态输入时 + 使用 `messages`。供应商专属参数放在 `extra_body` 或 Provider 配置的 + `options` 中,适配器会在请求边界进行校验和转换。未显式传入 + `system_prompt` 时沿用兼容默认值 `You are a helpful assistant.`; + 如需完全不带系统提示,请显式传入 `None`。 + """ + + prompt: str | None = None + system_prompt: str | None = "You are a helpful assistant." + messages: Sequence[Mapping[str, Any]] | None = None + tools: list[dict[str, Any]] | None = None + stream: bool = True + # Keep the historical public default while distinguishing an omitted + # value from an explicit ``temperature=0.7`` for model-level defaults. + temperature: float | None = _UNSET_TEMPERATURE # type: ignore[assignment] + _temperature_explicit: bool = dataclass_field( + init=False, default=False, repr=False, compare=False + ) + max_tokens: int | None = None + top_p: float | None = None + top_k: int | None = None + frequency_penalty: float | None = None + presence_penalty: float | None = None + repetition_penalty: float | None = None + n: int | None = None + logit_bias: dict[str, int] | None = None + logprobs: bool | None = None + top_logprobs: int | None = None + modalities: list[str] | None = None + audio: dict[str, Any] | None = None + prediction: dict[str, Any] | None = None + web_search_options: dict[str, Any] | None = None + seed: int | None = None + stop: str | Sequence[str] | None = None + response_format: dict[str, Any] | None = None + tool_choice: Any | None = None + extra_body: dict[str, Any] | None = None + extra_headers: dict[str, str] | None = None + extra_query: dict[str, Any] | None = None + metadata: dict[str, Any] | None = None + user: str | None = None + timeout: float | None = None + # OpenAI Responses/Ark Responses 的会话与推理参数。其它 Provider 会在 + # 请求边界忽略不适用字段,但不会把它们误传给各自的 SDK。 + previous_response_id: str | None = None + # OpenAI Responses input token count 的模型风格预设。当前 SDK 仅在 + # `responses.input_tokens.count` 支持该字段,普通 Responses 创建会显式拒绝。 + personality: str | None = None + include: Sequence[str] | None = None + reasoning: dict[str, Any] | None = None + thinking: dict[str, Any] | None = None + store: bool | None = None + background: bool | None = None + parallel_tool_calls: bool | None = None + max_tool_calls: int | None = None + conversation: str | dict[str, Any] | None = None + session: dict[str, Any] | None = None + context_management: dict[str, Any] | None = None + caching: dict[str, Any] | None = None + expire_at: int | None = None + prompt_cache_options: dict[str, Any] | None = None + safety_identifier: str | None = None + moderation: dict[str, Any] | None = None + verbosity: str | None = None + prompt_cache_key: str | None = None + prompt_cache_retention: str | None = None + service_tier: str | None = None + truncation: str | None = None + stream_options: dict[str, Any] | None = None + # Anthropic Messages/Beta Messages 的资源级请求字段。它们保留在统一 + # 请求对象中,只有对应 Provider 会转发到 SDK。 + cache_control: dict[str, Any] | None = None + container: str | dict[str, Any] | None = None + inference_geo: str | None = None + mcp_servers: Sequence[Mapping[str, Any]] | None = None + output_config: dict[str, Any] | None = None + output_format: dict[str, Any] | None = None + speed: str | None = None + betas: Sequence[str] | None = None + diagnostics: dict[str, Any] | None = None + fallback_credit_token: str | None = None + fallbacks: Sequence[Mapping[str, Any]] | None = None + + def __post_init__(self) -> None: + """在统一请求边界校验并复制 HTTP 扩展,避免凭证绕过 Provider。""" + + self._temperature_explicit = self.temperature is not _UNSET_TEMPERATURE + if not self._temperature_explicit: + self.temperature = 0.7 + + headers, query = validate_secret_free_request_overrides( + self.extra_headers, + self.extra_query, + "CompletionRequest", + ) + self.extra_headers = headers # type: ignore[assignment] + self.extra_query = query + if self.extra_body is not None: + self.extra_body = _validate_secret_free_payload( + self.extra_body, + "CompletionRequest", + "extra_body", + ) + + @property + def temperature_is_explicit(self) -> bool: + """返回调用方是否显式提供了温度参数。""" + return self._temperature_explicit + + def copy_with(self, **changes: Any) -> "CompletionRequest": + """复制请求并保留隐式温度的显式性标记。 + + ``dataclasses.replace`` 会把已经规范化的 ``0.7`` 再当成显式值, + 从而覆盖模型级 temperature。Provider 在切换 stream 或调整其它 + 请求字段时应使用此方法,避免复制过程改变调用方语义。 + """ + if not self._temperature_explicit and "temperature" not in changes: + changes["temperature"] = _UNSET_TEMPERATURE + return replace(self, **changes) + + def to_invoke_kwargs(self) -> dict[str, Any]: + """转换为 Provider `invoke` 的关键字参数。""" + values = { + "prompt": self.prompt, + "system_prompt": self.system_prompt, + "messages": self.messages, + "tools": self.tools, + "stream": self.stream, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "top_p": self.top_p, + "top_k": self.top_k, + "frequency_penalty": self.frequency_penalty, + "presence_penalty": self.presence_penalty, + "repetition_penalty": self.repetition_penalty, + "n": self.n, + "logit_bias": self.logit_bias, + "logprobs": self.logprobs, + "top_logprobs": self.top_logprobs, + "modalities": self.modalities, + "audio": self.audio, + "prediction": self.prediction, + "web_search_options": self.web_search_options, + "seed": self.seed, + "stop": self.stop, + "response_format": self.response_format, + "tool_choice": self.tool_choice, + "extra_body": self.extra_body, + "extra_headers": self.extra_headers, + "extra_query": self.extra_query, + "metadata": self.metadata, + "user": self.user, + "timeout": self.timeout, + "previous_response_id": self.previous_response_id, + "personality": self.personality, + "include": self.include, + "reasoning": self.reasoning, + "thinking": self.thinking, + "store": self.store, + "background": self.background, + "parallel_tool_calls": self.parallel_tool_calls, + "max_tool_calls": self.max_tool_calls, + "conversation": self.conversation, + "session": self.session, + "context_management": self.context_management, + "caching": self.caching, + "expire_at": self.expire_at, + "prompt_cache_options": self.prompt_cache_options, + "safety_identifier": self.safety_identifier, + "moderation": self.moderation, + "verbosity": self.verbosity, + "prompt_cache_key": self.prompt_cache_key, + "prompt_cache_retention": self.prompt_cache_retention, + "service_tier": self.service_tier, + "truncation": self.truncation, + "stream_options": self.stream_options, + "cache_control": self.cache_control, + "container": self.container, + "inference_geo": self.inference_geo, + "mcp_servers": self.mcp_servers, + "output_config": self.output_config, + "output_format": self.output_format, + "speed": self.speed, + "betas": self.betas, + "diagnostics": self.diagnostics, + "fallback_credit_token": self.fallback_credit_token, + "fallbacks": self.fallbacks, + } + if not self._temperature_explicit: + values.pop("temperature", None) + return {key: value for key, value in values.items() if value is not None} + + +@dataclass +class CompletionResult: + """非流式或聚合后的统一聊天结果。""" + + text: str = "" + tool_calls: list[dict[str, Any]] = dataclass_field(default_factory=list) + usage: dict[str, Any] = dataclass_field(default_factory=dict) + finish_reason: str | None = None + response_id: str | None = None + refusal: str | None = None + reasoning: str = "" + raw: Any = None + + +@dataclass +class StreamEvent: + """统一流式事件。 + + `invoke`/`ainvoke` 仍只返回文本,以兼容现有聊天界面;需要工具调用、 + usage、reasoning 或完成原因时使用 `stream_events`/`astream_events`。 + """ + + type: str + text: str = "" + reasoning: str = "" + tool_call: dict[str, Any] | None = None + usage: dict[str, Any] = dataclass_field(default_factory=dict) + finish_reason: str | None = None + response_id: str | None = None + refusal: str | None = None + raw: Any = None + + +def merge_tool_call_fragment( + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + fallback_index: Any = None, +) -> tuple[Any, dict[str, Any]]: + """累积 OpenAI/Anthropic 风格的流式工具调用片段。 + + 供应商通常只在第一个片段发送 id/name,后续片段只发送 index 和 + partial arguments。统一在适配层保留这些字段,避免调用方自行拼接。 + """ + index = tool_call.get("index", fallback_index) + call_id = tool_call.get("id") or tool_call.get("call_id") + if index is not None: + key: Any = ("index", index) + elif call_id: + key = ("id", call_id) + else: + key = ("position", len(accumulator)) + + merged = accumulator.setdefault( + key, + { + "index": index, + "type": tool_call.get("type", "function") or "function", + "arguments": "", + }, + ) + if index is not None: + merged["index"] = index + for name in ("id", "call_id", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + + fragment = tool_call.get("arguments") + if isinstance(fragment, str): + if fragment: + current = merged.get("arguments", "") + merged["arguments"] = (current if isinstance(current, str) else "") + fragment + elif fragment is not None and not merged.get("arguments"): + merged["arguments"] = fragment + return key, merged + + +def normalize_messages( + prompt: str | None, + system_prompt: str | None, + messages: Sequence[Mapping[str, Any]] | None, +) -> list[dict[str, Any]]: + """生成不修改调用方对象的标准消息列表。""" + if messages is not None: + if isinstance(messages, (str, bytes, bytearray)) or not isinstance(messages, Sequence): + raise ValueError("messages 必须是消息对象序列。") + normalized: list[dict[str, Any]] = [] + for index, message in enumerate(messages): + if not isinstance(message, Mapping): + raise ValueError(f"messages[{index}] 必须是对象。") + normalized.append(dict(message)) + if system_prompt and not any(message.get("role") == "system" for message in normalized): + normalized.insert(0, {"role": "system", "content": system_prompt}) + return normalized + + if prompt is None or not str(prompt).strip(): + raise ValueError("聊天请求必须提供 prompt 或 messages。") + result: list[dict[str, Any]] = [{"role": "user", "content": prompt}] + if system_prompt: + result.insert(0, {"role": "system", "content": system_prompt}) + return result + + +def _responses_json_text(value: Any, location: str, *, allow_empty: bool = False) -> str: + """将工具参数或输出规范为 Responses 接受的 JSON 文本。""" + if value is None: + if allow_empty: + return "" + return "{}" + if isinstance(value, str): + if not value and allow_empty: + return "" + try: + parsed = json.loads(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{location} 必须是有效 JSON。") from exc + else: + parsed = value + try: + return json.dumps(parsed, ensure_ascii=False, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise ValueError(f"{location} 必须可序列化为 JSON。") from exc + + +def _responses_has_content(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value) + if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)): + return bool(value) + return True + + +def _responses_provider_variant(provider: str) -> str: + """规范化 Responses 目标 SDK 变体。""" + normalized = str(provider).strip().lower().replace("-", "_") + return "ark" if normalized in {"ark", "volcengine", "volc_engine"} else "openai" + + +def _responses_content_part( + part: Any, + location: str, + *, + provider: str = "openai", +) -> dict[str, Any]: + """将 Chat 风格多模态内容块转换为 Responses 内容块。 + + OpenAI/Ark Responses 不接受 Chat Completions 的 ``text``、 + ``image_url`` 和 ``file`` 类型;其输入内容分别使用 + ``input_text``、``input_image`` 和 ``input_file``。Ark 还支持 + ``input_audio``、``input_video`` 与 ``image_pixel_limit``,由 + ``provider`` 参数选择对应变体。不在目标 SDK 输入联合类型中的内容块 + 统一显式失败,不能把无效请求静默发送到服务端。 + """ + if isinstance(part, str): + return {"type": "input_text", "text": part} + if not isinstance(part, Mapping): + raise ValueError(f"{location} 必须是字符串或对象。") + + source = dict(part) + part_type = source.get("type") + variant = _responses_provider_variant(provider) + if part_type in {"text", "input_text", "output_text"}: + text = source.get("text") + if not isinstance(text, str): + raise ValueError(f"{location}.text 必须是字符串。") + converted = {"type": "input_text", "text": text} + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if part_type in {"audio_url", "input_audio"}: + if variant != "ark": + raise ValueError( + f"{location} 的 {part_type} 内容块当前不受 Responses SDK 支持。" + ) + audio_value = source.get("audio_url") + if isinstance(audio_value, Mapping): + audio_url = audio_value.get("url") or audio_value.get("audio_url") + audio_file_id = source.get("file_id") or audio_value.get("file_id") + chunking_strategy = source.get( + "chunking_strategy", audio_value.get("chunking_strategy") + ) + else: + audio_url = audio_value + audio_file_id = source.get("file_id") + chunking_strategy = source.get("chunking_strategy") + if not isinstance(audio_url, str) or not audio_url.strip(): + raise ValueError(f"{location} 缺少有效 audio_url。") + if audio_file_id is not None and not isinstance(audio_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if chunking_strategy is not None and not isinstance(chunking_strategy, Mapping): + raise ValueError(f"{location}.chunking_strategy 必须是对象。") + converted_audio: dict[str, Any] = {"type": "input_audio", "audio_url": audio_url} + if audio_file_id: + converted_audio["file_id"] = audio_file_id + if chunking_strategy is not None: + converted_audio["chunking_strategy"] = dict(chunking_strategy) + return converted_audio + + if part_type in {"video_url", "input_video"}: + if variant != "ark": + raise ValueError( + f"{location} 的 {part_type} 内容块当前不受 Responses SDK 支持。" + ) + video_value = source.get("video_url") + if isinstance(video_value, Mapping): + video_url = video_value.get("url") or video_value.get("video_url") + video_file_id = source.get("file_id") or video_value.get("file_id") + fps = source.get("fps", video_value.get("fps")) + else: + video_url = video_value + video_file_id = source.get("file_id") + fps = source.get("fps") + if video_url is not None and not isinstance(video_url, str): + raise ValueError(f"{location}.video_url.url 必须是字符串。") + if video_file_id is not None and not isinstance(video_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not video_url and not video_file_id: + raise ValueError(f"{location} 缺少 video_url 或 file_id。") + if fps is not None and ( + isinstance(fps, bool) + or not isinstance(fps, (int, float)) + or not math.isfinite(float(fps)) + ): + raise ValueError(f"{location}.fps 必须是有限数字。") + converted_video: dict[str, Any] = {"type": "input_video"} + if video_url: + converted_video["video_url"] = video_url + if video_file_id: + converted_video["file_id"] = video_file_id + if fps is not None: + converted_video["fps"] = fps + return converted_video + + if part_type == "image_url": + image_value = source.pop("image_url", None) + image_url: Any = None + image_file_id: Any = source.get("file_id") + detail: Any = source.get("detail") + image_pixel_limit: Any = source.get("image_pixel_limit") + if isinstance(image_value, Mapping): + image_url = image_value.get("url") or image_value.get("image_url") + image_file_id = image_file_id or image_value.get("file_id") + detail = detail or image_value.get("detail") + image_pixel_limit = image_pixel_limit or image_value.get("image_pixel_limit") + elif isinstance(image_value, str): + image_url = image_value + elif image_value is not None: + raise ValueError(f"{location}.image_url 必须是字符串或对象。") + fallback_url = source.pop("url", None) + image_url = image_url or fallback_url + if image_url is not None and not isinstance(image_url, str): + raise ValueError(f"{location}.image_url.url 必须是字符串。") + if image_file_id is not None and not isinstance(image_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not image_url and not image_file_id: + raise ValueError(f"{location} 缺少 image_url 或 file_id。") + if detail is None: + detail = "auto" + allowed_details = {"auto", "low", "high"} + if variant == "openai": + allowed_details.add("original") + if not isinstance(detail, str) or detail not in allowed_details: + allowed = "、".join(sorted(allowed_details)) + raise ValueError(f"{location}.detail 必须是 {allowed}。") + converted_image: dict[str, Any] = {"type": "input_image", "detail": detail} + if image_url: + converted_image["image_url"] = image_url + if image_file_id: + converted_image["file_id"] = image_file_id + if image_pixel_limit is not None: + if variant != "ark": + raise ValueError(f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。") + if not isinstance(image_pixel_limit, Mapping): + raise ValueError(f"{location}.image_pixel_limit 必须是对象。") + converted_image["image_pixel_limit"] = dict(image_pixel_limit) + if "prompt_cache_breakpoint" in source and variant == "openai": + converted_image["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted_image + + if part_type == "input_image": + image_value = source.get("image_url") + if isinstance(image_value, Mapping): + image_url = image_value.get("url") or image_value.get("image_url") + file_id = source.get("file_id") or image_value.get("file_id") + if source.get("detail") is None and image_value.get("detail") is not None: + source["detail"] = image_value["detail"] + if ( + source.get("image_pixel_limit") is None + and image_value.get("image_pixel_limit") is not None + ): + source["image_pixel_limit"] = image_value["image_pixel_limit"] + if image_url is not None: + source["image_url"] = image_url + else: + source.pop("image_url", None) + if file_id is not None: + source["file_id"] = file_id + if source.get("image_url") is not None and not isinstance(source["image_url"], str): + raise ValueError(f"{location}.image_url 必须是字符串。") + if source.get("file_id") is not None and not isinstance(source["file_id"], str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not source.get("image_url") and not source.get("file_id"): + raise ValueError(f"{location} 缺少 image_url 或 file_id。") + if source.get("detail") is None: + source["detail"] = "auto" + allowed_details = {"auto", "low", "high"} + if variant == "openai": + allowed_details.add("original") + if source["detail"] not in allowed_details: + allowed = "、".join(sorted(allowed_details)) + raise ValueError(f"{location}.detail 必须是 {allowed}。") + converted_input_image: dict[str, Any] = {"type": "input_image", "detail": source["detail"]} + if source.get("image_pixel_limit") is not None: + if variant != "ark": + raise ValueError(f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。") + if not isinstance(source["image_pixel_limit"], Mapping): + raise ValueError(f"{location}.image_pixel_limit 必须是对象。") + converted_input_image["image_pixel_limit"] = dict(source["image_pixel_limit"]) + for key in ("image_url", "file_id"): + if source.get(key) is not None: + converted_input_image[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted_input_image["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted_input_image + + if part_type == "file": + nested_file = source.pop("file", None) + nested: dict[str, Any] = {} + if isinstance(nested_file, Mapping): + nested.update(nested_file) + elif isinstance(nested_file, str): + if nested_file.startswith(("http://", "https://")): + nested["file_url"] = nested_file + else: + nested["file_id"] = nested_file + elif nested_file is not None: + raise ValueError(f"{location}.file 必须是字符串或对象。") + for key in ("file_id", "file_data", "file_url", "filename", "detail"): + if key not in source and key in nested: + source[key] = nested[key] + if "file_url" not in source and "url" in nested: + source["file_url"] = nested["url"] + if "file_url" not in source and isinstance(source.get("url"), str): + source["file_url"] = source["url"] + if not any(source.get(key) for key in ("file_id", "file_data", "file_url")): + raise ValueError( + f"{location} 缺少 file_id、file_data 或 file_url。" + ) + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None and not isinstance(source[key], str): + raise ValueError(f"{location}.{key} 必须是字符串。") + converted = {"type": "input_file"} + if source.get("detail") is not None: + if variant != "openai" or source["detail"] not in {"auto", "low", "high"}: + raise ValueError(f"{location}.detail 不受 {variant.title()} Responses SDK 支持。") + converted["detail"] = source["detail"] + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None: + converted[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if part_type == "input_file": + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None and not isinstance(source[key], str): + raise ValueError(f"{location}.{key} 必须是字符串。") + if not any(source.get(key) for key in ("file_id", "file_data", "file_url")): + raise ValueError( + f"{location} 缺少 file_id、file_data 或 file_url。" + ) + converted = {"type": "input_file"} + if source.get("detail") is not None: + if variant != "openai" or source["detail"] not in {"auto", "low", "high"}: + raise ValueError(f"{location}.detail 不受 {variant.title()} Responses SDK 支持。") + converted["detail"] = source["detail"] + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None: + converted[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if not isinstance(part_type, str) or not part_type.strip(): + raise ValueError(f"{location}.type 必须是非空字符串。") + raise ValueError( + f"{location} 的内容类型 {part_type} 不受 Responses SDK 支持。" + ) + + +def _responses_message_content( + value: Any, + location: str, + *, + provider: str = "openai", +) -> Any: + """复制并归一化 Responses 消息的 content 字段。""" + if value is None or isinstance(value, str): + return value + if isinstance(value, (bytes, bytearray)) or not isinstance(value, Sequence): + raise ValueError(f"{location}.content 必须是字符串或内容块序列。") + return [ + _responses_content_part( + part, + f"{location}.content[{index}]", + provider=provider, + ) + for index, part in enumerate(value) + ] + + +def _responses_function_call_item( + tool_call: Mapping[str, Any], + location: str, + *, + require_call_id: bool = False, +) -> dict[str, Any]: + """将 Chat Completions 工具调用转换为 Responses function_call item。""" + explicit_call_id = tool_call.get("call_id") + call_id = explicit_call_id + if call_id is None and not require_call_id: + call_id = tool_call.get("id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError(f"{location} 缺少 call_id/id。") + + function = tool_call.get("function") + if function is not None and not isinstance(function, Mapping): + raise ValueError(f"{location}.function 必须是对象。") + function_values = function if isinstance(function, Mapping) else tool_call + name = function_values.get("name") or tool_call.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{location} 缺少函数名。") + + arguments = function_values.get( + "arguments", + tool_call.get("arguments", tool_call.get("input")), + ) + item: dict[str, Any] = { + "type": "function_call", + "call_id": call_id, + "name": name, + "arguments": _responses_json_text(arguments, f"{location}.arguments"), + } + # A Chat Completions ``id`` is the call identifier, not the Responses + # output-item identifier. Do not duplicate it as ``id`` unless the caller + # supplied a distinct Responses-style ``call_id`` explicitly. + if explicit_call_id is not None and tool_call.get("id") is not None: + item["id"] = tool_call["id"] + if tool_call.get("status") is not None: + item["status"] = tool_call["status"] + return item + + +def _responses_function_output_item( + message: Mapping[str, Any], + location: str, + *, + require_call_id: bool = False, + provider: str = "openai", +) -> dict[str, Any]: + call_id = message.get("call_id") + if call_id is None and not require_call_id: + call_id = message.get("tool_call_id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError(f"{location} 缺少 tool_call_id/call_id。") + output = message.get("output", message.get("content")) + if output is None: + output_text = "" + elif isinstance(output, str): + output_text = output + elif isinstance(output, Sequence) and not isinstance(output, (bytes, bytearray)): + return { + "type": "function_call_output", + "call_id": call_id, + "output": [ + _responses_content_part( + part, + f"{location}.output[{index}]", + provider=provider, + ) + for index, part in enumerate(output) + ], + **( + {"status": message["status"]} + if "status" in message and message["status"] is not None + else {} + ), + } + else: + output_text = _responses_json_text(output, f"{location}.output") + item: dict[str, Any] = { + "type": "function_call_output", + "call_id": call_id, + "output": output_text, + } + if "status" in message and message["status"] is not None: + item["status"] = message["status"] + return item + + +def normalize_responses_input( + prompt: str | None, + system_prompt: str | None, + messages: Sequence[Mapping[str, Any]] | None, + *, + provider: str = "openai", +) -> str | list[dict[str, Any]]: + """将统一消息转换为 OpenAI/Ark Responses 输入项。 + + Chat Completions 的 assistant.tool_calls 与 role=tool 不是 Responses + 输入消息类型,必须分别变成 function_call 和 function_call_output。 + 原生 Responses item 会保留,但其关键字段仍在边界处校验。``provider`` + 为 ``ark``/``volcengine`` 时启用 Ark 独有的音视频内容块。 + """ + normalized = normalize_messages(prompt, system_prompt, messages) + if messages is None and prompt is not None: + return prompt + + converted: list[dict[str, Any]] = [] + for index, message in enumerate(normalized): + location = f"Responses messages[{index}]" + message_type = message.get("type") + if message_type == "function_call": + converted.append( + _responses_function_call_item( + message, + location, + require_call_id=True, + ) + ) + continue + if message_type == "function_call_output": + converted.append( + _responses_function_output_item( + message, + location, + require_call_id=True, + provider=provider, + ) + ) + continue + + role = message.get("role") + tool_calls = message.get("tool_calls") + if role == "assistant" and tool_calls is not None: + if ( + isinstance(tool_calls, (str, bytes, bytearray)) + or not isinstance(tool_calls, Sequence) + ): + raise ValueError(f"{location}.tool_calls 必须是工具调用序列。") + content = message.get("content") + if _responses_has_content(content): + assistant_message: dict[str, Any] = { + "role": "assistant", + "content": _responses_message_content( + content, + location, + provider=provider, + ), + } + for key in ("type", "status", "phase"): + if key in message: + assistant_message[key] = message[key] + converted.append(assistant_message) + for call_index, tool_call in enumerate(tool_calls): + if not isinstance(tool_call, Mapping): + raise ValueError( + f"{location}.tool_calls[{call_index}] 必须是对象。" + ) + converted.append( + _responses_function_call_item( + tool_call, + f"{location}.tool_calls[{call_index}]", + ) + ) + continue + if role == "tool": + converted.append( + _responses_function_output_item( + message, + location, + provider=provider, + ) + ) + continue + if role == "function": + raise ValueError(f"{location} 的 function 消息缺少 tool_call_id。") + + normalized_message = dict(message) + if "content" in normalized_message: + normalized_message["content"] = _responses_message_content( + normalized_message["content"], + location, + provider=provider, + ) + converted.append(normalized_message) + return converted + + +def has_system_message(messages: Sequence[Mapping[str, Any]] | None) -> bool: + """判断调用方是否已经在消息列表中提供 system 消息。""" + return bool(messages and any(message.get("role") == "system" for message in messages)) + + +def normalize_usage(usage: Any) -> dict[str, Any]: + """将常见供应商 usage 结构规范为统一 token 字段。""" + if usage is None: + return {} + def usage_key(prefix: str) -> str: + return f"{prefix}_tokens" + + aliases = { + "prompt_token_count": usage_key("input"), + "candidates_token_count": usage_key("output"), + "total_token_count": usage_key("total"), + "input_token_count": usage_key("input"), + "output_token_count": usage_key("output"), + "cached_content_token_count": usage_key("cached"), + "thoughts_token_count": usage_key("reasoning"), + "tool_use_prompt_token_count": usage_key("tool_use_prompt"), + } + result: dict[str, Any] = {} + for source, target in aliases.items(): + value = field(usage, source) + if value is not None: + result[target] = value + for key in ( + "prompt_tokens", "completion_tokens", "total_tokens", "input_tokens", + "output_tokens", "cached_tokens", "reasoning_tokens", + "tool_use_prompt_tokens", + "cache_creation_input_tokens", "cache_read_input_tokens", + ): + value = field(usage, key) + if value is not None: + result[key] = value + input_details = field(usage, "input_tokens_details") + output_details = field(usage, "output_tokens_details") + if (value := field(input_details, "cached_tokens")) is not None: + result.setdefault("cached_tokens", value) + if (value := field(output_details, "reasoning_tokens")) is not None: + result.setdefault("reasoning_tokens", value) + return result + + +def extract_system_prompt(messages: Sequence[Mapping[str, Any]] | None) -> str | None: + """提取消息列表中的 system 内容,供无 system role 的 SDK 使用。""" + if not messages: + return None + values = [content_to_text(message.get("content", "")) for message in messages if message.get("role") == "system"] + value = "\n\n".join(item for item in values if item) + return value or None + + +def content_to_text(content: Any) -> str: + """从常见 SDK 内容块中提取文本,保留未知块的显式空结果。""" + if isinstance(content, str): + return content + if isinstance(content, Mapping): + text = content.get("text") + return text if isinstance(text, str) else "" + if isinstance(content, Sequence) and not isinstance(content, (bytes, bytearray)): + return "".join(content_to_text(item) for item in content) + return "" + + +def normalize_tool_arguments(value: Any) -> str: + """将 Responses 工具参数统一为 JSON 字符串。""" + if value is None: + return "" + if isinstance(value, str): + return value + try: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise ValueError("工具调用 arguments 必须可序列化为 JSON。") from exc + + +def validate_complete_tool_call(tool_call: Mapping[str, Any], provider: str) -> dict[str, Any]: + """校验流式工具调用在发送 completed 事件前已经完整。""" + name = tool_call.get("name") + if not isinstance(name, str) or not name.strip(): + raise RuntimeError(f"{provider} 工具调用缺少 name,流提前结束。") + tool_type = tool_call.get("type", "function_call") + arguments = tool_call.get("arguments") + # Responses custom tools intentionally accept arbitrary text, including an + # empty input. Function calls still require a complete JSON arguments value. + if tool_type != "custom_tool_call" and (arguments is None or arguments == ""): + raise RuntimeError(f"{provider} 工具调用 {name} 缺少完整 arguments,流提前结束。") + if tool_type == "function_call" and isinstance(arguments, str): + try: + json.loads(arguments) + except json.JSONDecodeError as exc: + raise RuntimeError( + f"{provider} 工具调用 {name} 的 arguments 不是完整 JSON。" + ) from exc + return dict(tool_call) + + +def normalize_embedding_vector(value: Any) -> list[float]: + """将 SDK 返回的 list 或 OpenAI base64 embedding 统一为浮点列表。""" + if hasattr(value, "tolist"): + value = value.tolist() + if isinstance(value, (list, tuple)): + return [float(item) for item in value] + if isinstance(value, (bytes, bytearray, memoryview)): + encoded = bytes(value) + elif isinstance(value, str): + try: + encoded = base64.b64decode(value, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError("Embedding base64 内容无效。") from exc + else: + raise TypeError(f"Embedding 返回了不支持的类型: {type(value).__name__}") + + if not encoded or len(encoded) % 4: + raise ValueError("Embedding base64 内容不是有效的 float32 字节序列。") + count = len(encoded) // 4 + return list(struct.unpack(f"<{count}f", encoded)) + + +def field(value: Any, name: str, default: Any = None) -> Any: + """兼容 Pydantic SDK 对象和字典响应。""" + if isinstance(value, Mapping): + return value.get(name, default) + return getattr(value, name, default) + + +def is_retryable_error(error: BaseException) -> bool: + """只对连接、限流和服务端错误重试,避免参数/认证错误重复发送请求。""" + if isinstance(error, (TimeoutError, ConnectionError)): + return True + status_values = ( + field(error, "status_code"), + field(error, "code"), + field(field(error, "response"), "status_code"), + ) + for status in status_values: + if isinstance(status, bool): + continue + if isinstance(status, int): + return status == 429 or status >= 500 + if isinstance(status, str) and status.isdigit(): + numeric_status = int(status) + return numeric_status == 429 or numeric_status >= 500 + name = error.__class__.__name__.lower() + return any(token in name for token in ("timeout", "connection", "ratelimit", "internalserver", "serviceunavailable")) + + +def reject_unsupported_kwargs(provider: str, values: Mapping[str, Any]) -> None: + """拒绝适配器未实现的非空请求字段,避免参数被静默丢弃。""" + unsupported = sorted(key for key, value in values.items() if value is not None) + if unsupported: + raise ValueError(f"{provider} 不支持请求参数: {', '.join(unsupported)}") + + +def validate_secret_free_options( + options: Mapping[str, Any] | None, + provider: str, +) -> dict[str, Any]: + """校验直接构造 Provider 时的模型级选项不会携带凭证。""" + return _validate_secret_free_options(options, provider) + + +def validate_secret_free_request_overrides( + extra_headers: Mapping[str, Any] | None = None, + extra_query: Mapping[str, Any] | None = None, + provider: str = "Provider", +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """校验并复制请求级 HTTP 扩展。""" + + return _validate_secret_free_request_overrides( + extra_headers, + extra_query, + provider, + ) + + +def coerce_completion_request( + request: CompletionRequest | None, + kwargs: Mapping[str, Any], + operation: str = "请求", +) -> CompletionRequest: + """规范化统一请求,并拒绝混用对象参数和关键字参数。 + + 两种调用形式分别用于不同的调用场景。混用时如果静默丢弃关键字参数, + 调用方看到的请求和实际发送的请求会不一致,因此在进入 SDK 前显式失败。 + """ + if request is not None and kwargs: + raise ValueError(f"{operation} 不能同时传 request 和关键字参数。") + if request is not None: + return request + return CompletionRequest(**dict(kwargs)) + + +async def iterate_async(value: Any) -> AsyncGenerator[Any, None]: + """统一消费 SDK 的异步流和同步迭代器返回值。 + + 官方异步客户端通常返回 ``AsyncIterable``,但某些 SDK 版本、兼容网关 + 或测试替身会直接返回普通迭代器。两者都应保持同一异步 Provider 契约。 + """ + try: + if isinstance(value, AsyncIterable) or hasattr(value, "__aiter__"): + async for item in value: + yield item + return + if isinstance(value, Iterable): + for item in value: + yield item + return + raise TypeError("SDK 流式响应既不是异步迭代器也不是同步迭代器。") + finally: + close = getattr(value, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + else: + close = getattr(value, "close", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + + +T = TypeVar("T") +_STREAM_END = object() + + +class StreamEventError(RuntimeError): + """SSE 错误事件,保留 HTTP 状态供首帧安全重试判断。""" + + def __init__(self, message: str, status_code: int | None = None): + super().__init__(message) + self.status_code = status_code + + +def raise_for_stream_error_event(event: Any, provider: str) -> None: + """将协议层 error 事件转换为带状态码的异常。""" + event_type = field(event, "type", "") + embedded_error = field(event, "error") + if event_type not in {"error", "response.error"} and embedded_error is None: + return + error = embedded_error or event + pending: list[Any] = [error] + visited: set[int] = set() + message: str | None = None + while pending and not message: + current = pending.pop(0) + if current is None or id(current) in visited: + continue + visited.add(id(current)) + if isinstance(current, str) and current.strip(): + message = current.strip() + break + if isinstance(current, Mapping): + values = [current.get(key) for key in ("message", "detail", "reason", "error", "body")] + else: + values = [field(current, key) for key in ("message", "detail", "reason", "error", "body")] + for value in values: + if isinstance(value, str) and value.strip(): + message = value.strip() + break + if not message: + pending.extend(value for value in values if value is not None) + if not message: + message = field(error, "code") or f"{provider} 流返回错误" + status: int | None = None + for value in ( + field(error, "status_code"), + field(error, "status"), + field(event, "status_code"), + field(event, "status"), + ): + if isinstance(value, bool): + continue + if isinstance(value, int): + status = value + break + if isinstance(value, str) and value.isdigit(): + status = int(value) + break + raise StreamEventError(f"{provider} 流返回错误: {message}", status) + + +def retry_sync_call(call: Callable[[], T]) -> T: + """在生成器消费阶段重试同步 SDK 请求建立。""" + for attempt in Retrying( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True, + ): + with attempt: + return call() + raise RuntimeError("同步 SDK 请求重试未返回结果。") + + +async def retry_async_call(call: Callable[[], Any]) -> Any: + """在生成器消费阶段重试异步 SDK 请求建立。""" + async for attempt in AsyncRetrying( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True, + ): + with attempt: + result = call() + return await result if inspect.isawaitable(result) else result + raise RuntimeError("异步 SDK 请求重试未返回结果。") + + +def retry_sync_stream( + factory: Callable[[], Any], + first_event_validator: Callable[[Any], None] | None = None, +) -> Generator[Any, None, None]: + """只重试同步流在首个事件前的建立阶段,避免重复已输出内容。""" + def open_and_peek() -> tuple[Any, Any]: + iterator = iter(factory()) + try: + first = next(iterator) + if first is not _STREAM_END and first_event_validator is not None: + first_event_validator(first) + return iterator, first + except StopIteration: + return iterator, _STREAM_END + except BaseException: + close = getattr(iterator, "close", None) + if close is not None: + close() + raise + + iterator, first = retry_sync_call(open_and_peek) + try: + if first is not _STREAM_END: + yield first + yield from iterator + finally: + close = getattr(iterator, "close", None) + if close is not None: + close() + + +async def retry_async_stream( + factory: Callable[[], Any], + first_event_validator: Callable[[Any], None] | None = None, +) -> AsyncGenerator[Any, None]: + """只重试异步流在首个事件前的建立阶段,避免重复已输出内容。""" + async def open_and_peek() -> tuple[Any, Any]: + response = factory() + response = await response if inspect.isawaitable(response) else response + iterator = iterate_async(response).__aiter__() + try: + first = await iterator.__anext__() + if first is not _STREAM_END and first_event_validator is not None: + first_event_validator(first) + return iterator, first + except StopAsyncIteration: + return iterator, _STREAM_END + except BaseException: + close = getattr(iterator, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + raise + + iterator, first = await retry_async_call(open_and_peek) + try: + if first is not _STREAM_END: + yield first + async for item in iterator: + yield item + finally: + close = getattr(iterator, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + + +@contextmanager +def retry_sync_stream_context(factory: Callable[[], Any]) -> Any: + """重试同步流上下文的进入阶段,不重试已经开始的事件消费。""" + def enter() -> tuple[Any, Any]: + manager = factory() + try: + return manager, manager.__enter__() + except BaseException: + close = getattr(manager, "close", None) + if close is not None: + close() + raise + + manager, response = retry_sync_call(enter) + try: + yield response + except BaseException as error: + if not manager.__exit__(type(error), error, error.__traceback__): + raise + else: + manager.__exit__(None, None, None) + + +@asynccontextmanager +async def retry_async_stream_context(factory: Callable[[], Any]) -> Any: + """重试异步流上下文的进入阶段,不重试已经开始的事件消费。""" + async def enter() -> tuple[Any, Any]: + manager = factory() + try: + return manager, await manager.__aenter__() + except BaseException: + close = getattr(manager, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + raise + + manager, response = await retry_async_call(enter) + try: + yield response + except BaseException as error: + if not await manager.__aexit__(type(error), error, error.__traceback__): + raise + else: + await manager.__aexit__(None, None, None) + + +_RESOURCE_GUARD_EXCLUDED = frozenset( + { + # These entry points already coerce and validate a CompletionRequest; + # wrapping them would validate the same values twice and would make + # their streaming generators harder to introspect. + "complete", + "acomplete", + "invoke", + "ainvoke", + "stream_events", + "astream_events", + "embed_documents", + "aembed_documents", + "embed_query", + "aembed_query", + } +) + + +def _provider_label(provider: Any) -> str: + return str(getattr(provider, "_provider", None) or type(provider).__name__) + + +def _provider_call_accepts_arguments( + method: Callable[..., Any], + provider: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], +) -> bool: + """判断 Provider 自身的调用签名是否完整。""" + + try: + inspect.signature(method).bind(provider, *args, **dict(kwargs)) + except (TypeError, ValueError): + return False + return True + + +def _is_missing_required_argument_error(error: TypeError) -> bool: + message = str(error).lower() + return "missing" in message and "required" in message and "argument" in message + + +def _is_duplicate_argument_error(error: TypeError) -> bool: + """识别 SDK 调用中由显式参数与 ``**kwargs`` 重复造成的 TypeError。""" + + message = str(error).lower() + return "multiple values for" in message and ( + "argument" in message or "keyword" in message + ) + + +def _sanitize_resource_call( + method: Callable[..., Any], + provider: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], +) -> tuple[tuple[Any, ...], dict[str, Any]]: + """校验资源方法的显式扩展参数和 ``**kwargs``。""" + + label = f"{_provider_label(provider)} 资源" + copied_args = _validate_secret_free_resource_args(args, label) + copied_kwargs = dict(kwargs) + try: + signature = inspect.signature(method) + bound = signature.bind_partial(provider, *copied_args, **copied_kwargs) + except (TypeError, ValueError): + # Dynamic SDK wrappers can expose no inspectable signature. The + # keyword and positional paths are still safe and preserve the + # original call shape. + return copied_args, _validate_secret_free_resource_kwargs(copied_kwargs, label) + + extension_names = {"extra_headers", "extra_query", "extra_body"} + explicit = { + name: bound.arguments[name] + for name in extension_names + if name in bound.arguments + } + if explicit: + bound.arguments.update( + _validate_secret_free_resource_kwargs(explicit, label) + ) + for name, parameter in signature.parameters.items(): + if parameter.kind is inspect.Parameter.VAR_KEYWORD and name in bound.arguments: + bound.arguments[name] = _validate_secret_free_resource_kwargs( + bound.arguments[name], label + ) + # ``bound.args`` includes the instance we supplied for binding; the + # wrapper passes that instance separately when invoking the original + # function. + return bound.args[1:], dict(bound.kwargs) + + +def _guard_provider_resource_method(method: Callable[..., Any]) -> Callable[..., Any]: + """给 Provider 资源入口安装统一的请求扩展校验。""" + + if getattr(method, "_resource_guarded", False): + return method + if inspect.iscoroutinefunction(method): + + @wraps(method) + async def async_wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) + safe_args, safe_kwargs = _sanitize_resource_call( + method, provider, args, kwargs + ) + provider_call_is_valid = _provider_call_accepts_arguments( + method, provider, safe_args, safe_kwargs + ) + try: + return await method(provider, *safe_args, **safe_kwargs) + except TypeError as exc: + if "unexpected keyword argument" in str(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + if _is_duplicate_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数重复: {exc}" + ) from exc + if provider_call_is_valid and _is_missing_required_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + raise + + setattr(async_wrapper, "_resource_guarded", True) # noqa: B010 + return async_wrapper + + @wraps(method) + def wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) + safe_args, safe_kwargs = _sanitize_resource_call( + method, provider, args, kwargs + ) + provider_call_is_valid = _provider_call_accepts_arguments( + method, provider, safe_args, safe_kwargs + ) + try: + return method(provider, *safe_args, **safe_kwargs) + except TypeError as exc: + if "unexpected keyword argument" in str(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + if _is_duplicate_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数重复: {exc}" + ) from exc + if provider_call_is_valid and _is_missing_required_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + raise + + setattr(wrapper, "_resource_guarded", True) # noqa: B010 + return wrapper + + +def _is_provider_resource_method( + name: str, method: Any +) -> bool: + if name.startswith("_") or name in _RESOURCE_GUARD_EXCLUDED: + return False + if not callable(method): + return False + try: + parameters = inspect.signature(method).parameters.values() + except (TypeError, ValueError): + return False + return any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + or parameter.name in {"extra_headers", "extra_query", "extra_body"} + for parameter in parameters + ) + + +def close_resource_sync(resource: Any, label: str) -> None: + """在同步生命周期中可靠释放 Provider/服务资源。""" + close = getattr(resource, "close", None) + if not callable(close): + close = getattr(resource, "aclose", None) + if not callable(close): + return + result = close() + if not inspect.isawaitable(result): + return + try: + asyncio.get_running_loop() + except RuntimeError: + asyncio.run(_await_resource_close(result)) + return + # A synchronous close invoked from an active event loop cannot block on + # the async client without risking a nested-loop error. Close a coroutine + # object where possible to avoid a RuntimeWarning, then fail explicitly. + close_coroutine = getattr(result, "close", None) + if callable(close_coroutine): + close_coroutine() + raise RuntimeError(f"{label} 需要异步关闭;请调用 aclose()。") + + +async def _await_resource_close(result: Awaitable[Any]) -> None: + """将任意可等待关闭结果包装为 asyncio.run 可接受的协程。""" + await result class LargeLanguageModel(ABC): """语言模型抽象基类""" + capabilities = frozenset({"chat", "stream"}) + + def __init_subclass__(cls, **kwargs: Any) -> None: + """为 Provider 的原生资源入口统一安装安全边界。""" + + super().__init_subclass__(**kwargs) + for name, method in list(cls.__dict__.items()): + if _is_provider_resource_method(name, method): + setattr(cls, name, _guard_provider_resource_method(method)) + + @classmethod + def supports(cls, capability: str) -> bool: + return capability in cls.capabilities + + def _require_provider_resource(self, method_name: str) -> None: + """为 Provider 原生资源入口提供可选的能力门禁。""" + return + + @property + def sdk_client(self) -> Any: + """返回底层同步 SDK 客户端,供原生资源调用。""" + getter = getattr(self, "_get_client", None) + if getter is None: + raise AttributeError(f"{type(self).__name__} 没有同步 SDK 客户端。") + return getter() + + @property + def async_sdk_client(self) -> Any: + """返回底层异步 SDK 客户端,供原生资源调用。""" + getter = getattr(self, "_get_aclient", None) + if getter is not None: + client = getter() + if client is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + return client + client_getter = getattr(self, "_get_client", None) + client = client_getter() if client_getter is not None else None + if client is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + aio = getattr(client, "aio", None) + if aio is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + return aio + @abstractmethod def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7, ) -> Generator[str, None, None]: """同步调用语言模型。""" - pass @abstractmethod - async def ainvoke( + def ainvoke( self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: + ) -> AsyncIterator[str]: """异步调用语言模型。""" - pass + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """兼容层:聚合文本流;具体 Provider 可覆盖以返回 tool calls/usage。""" + request = coerce_completion_request(request, kwargs, "complete") + # 不修改调用方传入的 dataclass;Provider 的 complete 实现通常需要 + # 将流式请求改为非流式,但请求对象可能会被复用。 + request = request.copy_with(stream=False) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.invoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + text = "".join(self.invoke(**invoke_kwargs)) + return CompletionResult(text=text) + + async def acomplete( + self, request: CompletionRequest | None = None, **kwargs: Any + ) -> CompletionResult: + """异步兼容层;具体 Provider 可覆盖以返回完整元数据。""" + request = coerce_completion_request(request, kwargs, "acomplete") + request = request.copy_with(stream=False) + chunks: list[str] = [] + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.ainvoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + async for chunk in self.ainvoke(**invoke_kwargs): + chunks.append(chunk) + return CompletionResult(text="".join(chunks)) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + """以统一事件形式读取流;旧 Provider 默认包装文本流。""" + request = coerce_completion_request(request, kwargs, "stream_events") + request = request.copy_with(stream=True) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.invoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + for chunk in self.invoke(**invoke_kwargs): + yield StreamEvent(type="text_delta", text=chunk) + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + """异步统一事件流;旧 Provider 默认包装文本流。""" + request = coerce_completion_request(request, kwargs, "astream_events") + request = request.copy_with(stream=True) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.ainvoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + async for chunk in self.ainvoke(**invoke_kwargs): + yield StreamEvent(type="text_delta", text=chunk) + + def close(self) -> None: + """释放 Provider 持有的同步 SDK 客户端。""" + return + + async def aclose(self) -> None: + """释放 Provider 持有的异步 SDK 客户端。""" + return class TextEmbeddingModel(ABC): """文本向量化模型抽象基类""" @abstractmethod - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步将文档列表向量化。""" - pass @abstractmethod - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步将文档列表向量化。""" - pass + + def embed_query(self, text: str, **kwargs: Any) -> list[float]: + """默认的单文本查询向量化实现。""" + vectors = self.embed_documents([text], **kwargs) + if not vectors: + raise ValueError("Embedding provider 返回空结果。") + return vectors[0] + + async def aembed_query(self, text: str, **kwargs: Any) -> list[float]: + vectors = await self.aembed_documents([text], **kwargs) + if not vectors: + raise ValueError("Embedding provider 返回空结果。") + return vectors[0] + + def close(self) -> None: + """释放向量模型持有的同步资源;默认实现为空操作。""" + return + + async def aclose(self) -> None: + """释放向量模型持有的异步资源;默认实现为空操作。""" + return class RerankModel(ABC): """Rerank模型抽象基类""" @abstractmethod - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步对文档列表进行重排序。""" - pass @abstractmethod - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步对文档列表进行重排序。""" - pass \ No newline at end of file + + def close(self) -> None: + """释放重排模型持有的同步资源;默认实现为空操作。""" + return + + async def aclose(self) -> None: + """释放重排模型持有的异步资源;默认实现为空操作。""" + return diff --git a/src/providers/factory.py b/src/providers/factory.py index 447c6be..501de05 100644 --- a/src/providers/factory.py +++ b/src/providers/factory.py @@ -1,42 +1,183 @@ -# -*- coding: utf-8 -*- import importlib -from typing import Any, Dict, Type +import inspect +from collections.abc import Mapping +from functools import cache +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version +from typing import Any, ClassVar from src.providers.__base__.model_provider import ( LargeLanguageModel, RerankModel, TextEmbeddingModel, ) -from src.utils.config import get_settings # 导入 get_settings 函数 -from src.utils.log_manager import get_module_logger # 导入日志管理器 +from src.utils.config import ( # 导入 get_settings 函数 + ModelDetail, + ModelProtocol, + get_settings, +) +from src.utils.log_manager import get_module_logger # 导入日志管理器 +from src.utils.security import redact_sensitive_text, validate_secret_free_options logger = get_module_logger(__name__) # 获取当前模块的日志器 class ModelProviderFactory: """模型提供商工厂""" - _provider_map: Dict[str, Dict[str, Any]] = { + _roles = frozenset({"llm", "embedding", "rerank"}) + + _role_aliases: ClassVar[dict[str, dict[str, str]]] = { + "rerank": { + # SiliconFlow 的聊天/嵌入和 Rerank 使用不同的协议实现。 + "siliconflow": "siliconflow_rerank", + } + } + + _provider_map: ClassVar[dict[str, dict[str, Any]]] = { # LLM & Embedding Providers - "google": {"module": "src.providers.google", "class": "GoogleProvider"}, - "openai": {"module": "src.providers.openai", "class": "OpenAIProvider"}, - "anthropic": {"module": "src.providers.anthropic", "class": "AnthropicProvider"}, - "qwen": {"module": "src.providers.qwen", "class": "QwenProvider"}, - "volcengine": {"module": "src.providers.volcengine", "class": "VolcengineProvider"}, - "siliconflow": {"module": "src.providers.siliconflow", "class": "SiliconflowProvider"}, - "ollama": {"module": "src.providers.ollama", "class": "OllamaProvider"}, - "lm-studio": {"module": "src.providers.lm_studio", "class": "LMStudioProvider"}, - "deepseek": {"module": "src.providers.deepseek", "class": "DeepSeekProvider"}, - "grok": {"module": "src.providers.grok", "class": "GrokProvider"}, - "local-hash": {"module": "src.providers.local_hash", "class": "LocalHashEmbeddingProvider"}, + "google": { + "module": "src.providers.google", + "class": "GoogleProvider", + "sdk_package": "google-genai", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": {ModelProtocol.GENERATE_CONTENT.value}, + }, + "openai": { + "module": "src.providers.openai", + "class": "OpenAIProvider", + "sdk_package": "openai", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + # 这里不把 SDK 契约冒充成任意配置 Base URL 的服务端验收。 + # `protocol_status` 仅在显式登记真实渠道证据后返回 true。 + "server_verified_protocols": set(), + }, + "anthropic": { + "module": "src.providers.anthropic", + "class": "AnthropicProvider", + "sdk_package": "anthropic", + "integration": "native_sdk", + "capabilities": {"llm"}, + "protocols": {ModelProtocol.MESSAGES.value}, + }, + "qwen": { + "module": "src.providers.qwen", + "class": "QwenProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "volcengine": { + "module": "src.providers.volcengine", + "class": "VolcengineProvider", + "sdk_package": "volcengine-python-sdk", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.ARK.value, + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "siliconflow": { + "module": "src.providers.siliconflow", + "class": "SiliconflowProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "ollama": { + "module": "src.providers.ollama", + "class": "OllamaProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "lm-studio": { + "module": "src.providers.lm_studio", + "class": "LMStudioProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "deepseek": { + "module": "src.providers.deepseek", + "class": "DeepSeekProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "grok": { + "module": "src.providers.grok", + "class": "GrokProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "local-hash": { + "module": "src.providers.local_hash", + "class": "LocalHashEmbeddingProvider", + "integration": "local", + "capabilities": {"embedding"}, + }, # Rerank Providers - "jina": {"module": "src.providers.jina", "class": "JinaProvider"}, - "siliconflow_rerank": {"module": "src.providers.siliconflow_rerank", "class": "SiliconflowRerankProvider"}, + "jina": { + "module": "src.providers.jina", + "class": "JinaProvider", + "sdk_package": "httpx", + "integration": "http_json", + "capabilities": {"rerank"}, + }, + "siliconflow_rerank": { + "module": "src.providers.siliconflow_rerank", + "class": "SiliconflowRerankProvider", + "sdk_package": "httpx", + "integration": "http_json", + "capabilities": {"rerank"}, + }, } @staticmethod - def _get_provider_class(provider_name: str) -> Type: - """动态导入并返回提供商类""" + @cache + def _get_provider_class(provider_name: str) -> type: + """动态导入并缓存提供商类,避免每次创建实例都重复导入模块。""" if provider_name not in ModelProviderFactory._provider_map: logger.error(f"不支持的模型提供商: {provider_name}") raise ValueError(f"不支持的模型提供商: {provider_name}") @@ -48,80 +189,402 @@ def _get_provider_class(provider_name: str) -> Type: logger.debug(f"成功加载提供商类: {provider_info['class']} from {provider_info['module']}") return ProviderClass except ImportError as e: - logger.error(f"导入提供商模块失败: {provider_info['module']} - {e}") + logger.error( + "导入提供商模块失败: %s - %s", + provider_info["module"], + redact_sensitive_text(str(e)), + ) raise ImportError(f"无法加载提供商模块: {provider_info['module']}") from e except AttributeError as e: - logger.error(f"在模块中找不到提供商类: {provider_info['class']} in {provider_info['module']} - {e}") + logger.error( + "在模块中找不到提供商类: %s in %s - %s", + provider_info["class"], + provider_info["module"], + redact_sensitive_text(str(e)), + ) raise AttributeError(f"无法找到提供商类: {provider_info['class']}") from e @staticmethod - def get_llm_provider(provider_key: str) -> LargeLanguageModel: - """获取一个语言模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.llm_configurations: - logger.error(f"在LLM配置中未找到key: {provider_key}") - raise ValueError(f"在LLM配置中未找到key: {provider_key}") + def _resolve_provider_name(provider_name: str, role: str) -> str: + return ModelProviderFactory._role_aliases.get(role, {}).get(provider_name, provider_name) + + @classmethod + def _provider_info_for_role(cls, provider_name: str, role: str) -> tuple[str, dict[str, Any]]: + normalized_role = str(role).strip().lower() + if normalized_role not in cls._roles: + allowed = ", ".join(sorted(cls._roles)) + raise ValueError(f"不支持的模型角色: {role}。可选值: {allowed}") + resolved = cls._resolve_provider_name(provider_name, normalized_role) + info = cls._provider_map.get(resolved) + if info is None: + raise ValueError(f"不支持的模型提供商: {provider_name}") + capabilities = info.get("capabilities", set()) + if normalized_role not in capabilities: + raise TypeError(f"提供商 {provider_name} 不支持 {normalized_role} 能力。") + return resolved, info + + @staticmethod + def _create_provider( + provider_name: str, + model_name: str, + role: str, + expected_type: type, + protocol: ModelProtocol | str | None = None, + options: Mapping[str, Any] | None = None, + ) -> Any: + # ``ModelDetail`` normally normalizes protocol values before they reach + # the factory, but this internal helper is also used directly by + # integrations and tests. Normalize again at the boundary so a plain + # string does not fail later with ``AttributeError: value``. + protocol_value: str | None = None + if protocol is not None: + normalized_protocol = ModelDetail.normalize_protocol(protocol) + if normalized_protocol is None: + raise ValueError("provider protocol 不能为空。") + protocol_value = normalized_protocol.value + resolved_provider_name = ModelProviderFactory._resolve_provider_name(provider_name, role) + provider_info = ModelProviderFactory._provider_map.get(resolved_provider_name) + if provider_info is not None: + capabilities = provider_info.get("capabilities") + if capabilities is not None and role not in capabilities: + raise TypeError(f"提供商 {provider_name} 不支持 {role} 能力。") + if protocol_value is not None: + supported_protocols = provider_info.get("protocols", set()) + if protocol_value not in supported_protocols: + raise ValueError(f"提供商 {provider_name} 不支持 {protocol_value} 协议。") + provider_class = ModelProviderFactory._get_provider_class(resolved_provider_name) + if not issubclass(provider_class, expected_type): + raise TypeError( + f"提供商 {provider_name} 不支持 {role} 能力," + f"需要实现 {expected_type.__name__}。" + ) + provider_options = dict(options or {}) + parameters = inspect.signature(provider_class).parameters + accepts_options = "options" in parameters or any( + parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values() + ) + kwargs: dict[str, Any] = {"model_name": model_name} + if accepts_options: + kwargs["options"] = provider_options + elif provider_options: + unsupported = ", ".join(sorted(str(key) for key in provider_options)) + raise ValueError( + f"提供商 {provider_name} 的构造函数不支持 options,但配置提供了: {unsupported}" + ) + if protocol_value is None: + instance = provider_class(**kwargs) + return instance + + # Google/Anthropic/Volcengine 的协议固定在各自 SDK 中,不接受 + # protocol 构造参数;OpenAI 兼容 provider 才需要显式选择协议。 + parameters = inspect.signature(provider_class).parameters + if "protocol" not in parameters and not any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + for parameter in parameters.values() + ): + if ( + protocol_value is not None + and provider_info is not None + and protocol_value not in provider_info.get("protocols", set()) + ): + raise ValueError( + f"提供商 {provider_name} 的构造函数不支持 protocol={protocol_value}。" + ) + instance = provider_class(**kwargs) + return instance + kwargs["protocol"] = protocol_value + instance = provider_class(**kwargs) + if protocol_value == ModelProtocol.RESPONSES.value: + # OpenAI-compatible adapters need explicit remote capability evidence + # before exposing Responses resources. Native providers (for example + # Google and Anthropic) do not implement this guard. + require = getattr(instance, "_require_responses_resource", None) + if callable(require): + require("factory") + return instance + + @staticmethod + def _runtime_verified_protocols( + options: Mapping[str, Any] | None, + supported_protocols: set[str] | frozenset[str] | None = None, + provider_name: str | None = None, + ) -> frozenset[str]: + """读取诊断调用使用的实例级服务端协议登记。""" + if options is None: + return frozenset() + if str(provider_name or "").strip().lower() == "google" and "http_options" in options: + # ``http_options`` is a supported google-genai SDK container. Keep + # the general options boundary strict while allowing only this + # explicitly supported container and its safe trace headers. + remaining = { + key: value for key, value in options.items() if key != "http_options" + } + validated = validate_secret_free_options(remaining, "Provider") + validated["http_options"] = validate_secret_free_options( + {"http_options": options["http_options"]}, + "Provider", + allowed_containers={"http_options", "headers"}, + )["http_options"] + else: + validated = validate_secret_free_options(options, "Provider") + configured = validated.get("server_verified_protocols", ()) + if isinstance(configured, str): + configured = (configured,) + if not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError("server_verified_protocols 必须是字符串或协议序列。") + aliases = { + "chat": ModelProtocol.CHAT_COMPLETIONS.value, + "completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS.value, + "response": ModelProtocol.RESPONSES.value, + "anthropic": ModelProtocol.MESSAGES.value, + "anthropic_messages": ModelProtocol.MESSAGES.value, + "gemini": ModelProtocol.GENERATE_CONTENT.value, + "generate": ModelProtocol.GENERATE_CONTENT.value, + } + normalized: set[str] = set() + for value in configured: + protocol = str(value).strip().lower().replace("-", "_") + protocol = aliases.get(protocol, protocol) + try: + normalized_protocol = ModelProtocol(protocol).value + except ValueError as exc: + supported = ", ".join(item.value for item in ModelProtocol) + raise ValueError( + f"不支持的 server_verified_protocols 协议: {value}。可选值: {supported}" + ) from exc + if supported_protocols is not None and normalized_protocol not in supported_protocols: + supported = ", ".join(sorted(supported_protocols)) or "无" + raise ValueError( + f"server_verified_protocols 中的协议 {value} 不适用于当前提供商;" + f"支持: {supported}" + ) + normalized.add(normalized_protocol) + return frozenset(normalized) + + @classmethod + def capabilities(cls, provider_name: str, role: str = "llm") -> frozenset[str]: + """返回指定角色的渠道能力集合,供 UI/配置校验使用。""" + normalized_role = str(role).strip().lower() + resolved, _info = cls._provider_info_for_role(provider_name, normalized_role) + provider_class = cls._get_provider_class(resolved) + if normalized_role in {"embedding", "rerank"}: + return frozenset({normalized_role}) + declared: Any = getattr(provider_class, "capabilities", frozenset()) + # The factory's role map is the authoritative source for which model + # kinds can be instantiated. Do not leak a shared adapter's embedding + # or rerank methods into an LLM capability report. + return (frozenset(declared) - {"embedding", "rerank"}) | {normalized_role} + + @classmethod + def capability_report( + cls, + provider_name: str, + role: str = "llm", + options: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + """返回可供配置界面和诊断使用的机器可读能力报告。 + + ``capabilities`` 表示本地适配器已经实现的能力;协议的 + ``server_verified`` 仍只表示目标服务端有明确验证证据,不会由 SDK + 的本地资源树自动推导。 + """ + normalized_role = str(role).strip().lower() + resolved, info = cls._provider_info_for_role(provider_name, normalized_role) + provider_class = cls._get_provider_class(resolved) + sdk_package = info.get("sdk_package") + try: + sdk_version = package_version(sdk_package) if sdk_package else None + except PackageNotFoundError: + sdk_version = None + # `protocols` describes LLM wire protocols. Embedding and Rerank use + # their own endpoint contracts and must not inherit chat protocol names. + protocols = sorted(info.get("protocols", set())) if normalized_role == "llm" else [] + verified = set(info.get("server_verified_protocols", set())) + verified.update( + cls._runtime_verified_protocols( + options, + set(info.get("protocols", set())), + resolved, + ) + ) + verified_list = sorted(verified) + return { + "provider": provider_name, + "resolved_provider": resolved, + "role": normalized_role, + "provider_class": f"{provider_class.__module__}.{provider_class.__name__}", + "integration": info.get("integration", "unknown"), + "sdk": { + "package": sdk_package, + "version": sdk_version, + }, + "capabilities": sorted(cls.capabilities(provider_name, normalized_role)), + "protocols": protocols, + "server_verified_protocols": verified_list, + "protocol_status": { + protocol: { + "adapter_supported": True, + "server_verified": protocol in verified, + } + for protocol in protocols + }, + } + + @classmethod + def protocol_status( + cls, + provider_name: str, + protocol: str, + options: Mapping[str, Any] | None = None, + role: str | None = None, + ) -> dict[str, bool]: + """区分本地适配器支持与目标服务端已验证的协议。 + + LLM 是默认角色;未显式指定时,对 embedding/rerank-only provider + 自动选择其唯一角色,以便诊断工具可以统一查询所有渠道。 + """ + if role is None: + candidates = ("llm", "embedding", "rerank") + normalized_role = next( + ( + candidate + for candidate in candidates + if candidate in cls._provider_map.get( + cls._resolve_provider_name(provider_name, candidate), {} + ).get("capabilities", set()) + ), + None, + ) + if normalized_role is None: + raise ValueError(f"无法为提供商 {provider_name} 推断模型角色,请显式传入 role。") + else: + normalized_role = str(role).strip().lower() + resolved, info = cls._provider_info_for_role(provider_name, normalized_role) + normalized = str(protocol).strip().lower().replace("-", "_") + normalized = { + "chat": ModelProtocol.CHAT_COMPLETIONS.value, + "completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completions": ModelProtocol.CHAT_COMPLETIONS.value, + "response": ModelProtocol.RESPONSES.value, + "responses": ModelProtocol.RESPONSES.value, + "anthropic": ModelProtocol.MESSAGES.value, + "anthropic_messages": ModelProtocol.MESSAGES.value, + "gemini": ModelProtocol.GENERATE_CONTENT.value, + "generate": ModelProtocol.GENERATE_CONTENT.value, + "generate_content": ModelProtocol.GENERATE_CONTENT.value, + }.get(normalized, normalized) + adapter_supported = normalized in info.get("protocols", set()) + verified = set(info.get("server_verified_protocols", set())) + verified.update( + cls._runtime_verified_protocols( + options, + set(info.get("protocols", set())) if normalized_role == "llm" else set(), + resolved, + ) + ) + server_verified = normalized in verified + return {"adapter_supported": adapter_supported, "server_verified": server_verified} - config = current_settings.llm_configurations[provider_key] + @staticmethod + def _get_model_detail( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None, + settings_field: str, + role_label: str, + ) -> ModelDetail: + if configurations is None: + configurations = getattr(get_settings(), settings_field) + if provider_key not in configurations: + logger.error(f"在{role_label}配置中未找到key: {provider_key}") + raise ValueError(f"在{role_label}配置中未找到key: {provider_key}") + return configurations[provider_key] + + @staticmethod + def get_llm_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> LargeLanguageModel: + """获取一个语言模型提供商实例""" + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "llm_configurations", "LLM" + ) provider_name = config.provider model_name = config.model_name logger.info(f"正在获取LLM提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "llm", LargeLanguageModel, config.protocol, config.options + ) logger.info(f"成功获取LLM提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取LLM提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取LLM提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise @staticmethod - def get_embedding_provider(provider_key: str) -> TextEmbeddingModel: + def get_embedding_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> TextEmbeddingModel: """获取一个文本向量化模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.embedding_configurations: - logger.error(f"在Embedding配置中未找到key: {provider_key}") - raise ValueError(f"在Embedding配置中未找到key: {provider_key}") - - config = current_settings.embedding_configurations[provider_key] + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "embedding_configurations", "Embedding" + ) provider_name = config.provider model_name = config.model_name logger.info(f"正在获取Embedding提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + if config.protocol is not None: + raise ValueError("Embedding 配置不支持 protocol;协议只能配置在 llm_configurations 中。") + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "embedding", TextEmbeddingModel, options=config.options + ) logger.info(f"成功获取Embedding提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取Embedding提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取Embedding提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise @staticmethod - def get_rerank_provider(provider_key: str) -> RerankModel: + def get_rerank_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> RerankModel: """获取一个Rerank模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.rerank_configurations: - logger.error(f"在Rerank配置中未找到key: {provider_key}") - raise ValueError(f"在Rerank配置中未找到key: {provider_key}") - - config = current_settings.rerank_configurations[provider_key] - # Rerank提供商的key可能与LLM/Embedding提供商的key冲突(如siliconflow) - # 因此,我们在这里使用一个特殊的key,或者直接在配置中指定provider_map的key - # 为了简单起见,我们假设rerank的provider name是唯一的 + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "rerank_configurations", "Rerank" + ) provider_name = config.provider - if provider_name == "siliconflow": - provider_name = "siliconflow_rerank" # 映射到唯一的rerank provider - model_name = config.model_name logger.info(f"正在获取Rerank提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + if config.protocol is not None: + raise ValueError("Rerank 配置不支持 protocol;协议只能配置在 llm_configurations 中。") + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "rerank", RerankModel, options=config.options + ) logger.info(f"成功获取Rerank提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取Rerank提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取Rerank提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise diff --git a/src/utils/config.py b/src/utils/config.py index b9ada26..5d5bddb 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1,14 +1,20 @@ -# -*- coding: utf-8 -*- import functools import sys import tomllib +from collections.abc import Callable, Mapping from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, ClassVar, cast -from pydantic import BaseModel, Field, field_validator, ValidationError +from pydantic import BaseModel, Field, ValidationInfo, field_validator from pydantic.fields import FieldInfo -from pydantic_settings import BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource +from pydantic_settings import ( + BaseSettings, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +from src.utils.security import validate_secret_free_options # ================================================================= # 1. 基础定义 (DEFINITIONS) @@ -37,12 +43,84 @@ class RetrievalMethod(str, Enum): FULL_TEXT_SEARCH = "全文检索" HYBRID_SEARCH = "混合检索" + +class ModelProtocol(str, Enum): + """模型渠道使用的线协议。""" + + CHAT_COMPLETIONS = "chat_completions" + RESPONSES = "responses" + MESSAGES = "messages" + GENERATE_CONTENT = "generate_content" + ARK = "ark" + + class ModelDetail(BaseModel): """定义单个模型配置的结构。""" provider: str model_name: str + protocol: ModelProtocol | None = None + options: dict[str, Any] = Field(default_factory=dict) - model_config = SettingsConfigDict(protected_namespaces=()) + model_config = SettingsConfigDict( + protected_namespaces=(), + extra="forbid", + ) + + @field_validator("options", mode="before") + @classmethod + def validate_options(cls, value: Any, info: ValidationInfo) -> dict[str, Any]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError("模型 options 必须是 TOML 表或字典。") + provider = str(info.data.get("provider", "")).strip().lower() + try: + if provider == "google" and "http_options" in value: + http_options = value["http_options"] + remaining = { + key: nested + for key, nested in value.items() + if key != "http_options" + } + validated = validate_secret_free_options(remaining, "模型") + validated["http_options"] = validate_secret_free_options( + {"http_options": http_options}, + "模型", + allowed_containers={"http_options", "headers"}, + )["http_options"] + return validated + return validate_secret_free_options(value, "模型") + except ValueError as exc: + raise ValueError(str(exc).replace("模型 options 不允许包含凭证或请求头/query 配置", "模型 options 不允许包含凭证或连接字段")) from exc + + @field_validator("protocol", mode="before") + @classmethod + def normalize_protocol(cls, value: Any) -> ModelProtocol | None: + if value is None or value == "": + return None + if isinstance(value, ModelProtocol): + return value + + normalized = str(value).strip().lower().replace("-", "_") + aliases = { + "chat": ModelProtocol.CHAT_COMPLETIONS, + "completion": ModelProtocol.CHAT_COMPLETIONS, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS, + "chat_completions": ModelProtocol.CHAT_COMPLETIONS, + "response": ModelProtocol.RESPONSES, + "responses": ModelProtocol.RESPONSES, + "anthropic": ModelProtocol.MESSAGES, + "anthropic_messages": ModelProtocol.MESSAGES, + "gemini": ModelProtocol.GENERATE_CONTENT, + "generate": ModelProtocol.GENERATE_CONTENT, + "generate_content": ModelProtocol.GENERATE_CONTENT, + } + try: + alias = aliases.get(normalized) + return alias if alias is not None else ModelProtocol(normalized) + except ValueError as exc: + supported = ", ".join(protocol.value for protocol in ModelProtocol) + raise ValueError(f"不支持的模型协议: {value}。可选值: {supported}") from exc # ================================================================= # 2. 主配置模型 (MAIN SETTINGS MODEL) @@ -54,26 +132,68 @@ class Settings(BaseSettings): 加载顺序: 环境变量 > .env 文件 > config.toml 文件 > 模型中定义的默认值。 """ # --- [API_KEYS] --- - anthropic_api_key: Optional[str] = None - google_api_key: Optional[str] = None - siliconflow_api_key: Optional[str] = None - openai_api_key: Optional[str] = None - qwen_api_key: Optional[str] = None - volc_access_key: Optional[str] = None - volc_secret_key: Optional[str] = None - jina_api_key: Optional[str] = None - deepseek_api_key: Optional[str] = None - grok_api_key: Optional[str] = None - lm_studio_api_key: Optional[str] = "lm-studio" + anthropic_api_key: str | None = Field(default=None, repr=False) + google_api_key: str | None = Field(default=None, repr=False) + gemini_api_key: str | None = Field(default=None, repr=False) + google_genai_use_vertexai: bool | None = Field(default=None) + google_genai_use_enterprise: bool | None = Field(default=None) + google_cloud_project: str | None = Field(default=None) + google_cloud_location: str | None = Field(default=None) + google_application_credentials: str | None = Field(default=None, repr=False) + siliconflow_api_key: str | None = Field(default=None, repr=False) + openai_api_key: str | None = Field(default=None, repr=False) + qwen_api_key: str | None = Field(default=None, repr=False) + ark_api_key: str | None = Field(default=None, repr=False) + volc_access_key: str | None = Field(default=None, repr=False) + volc_secret_key: str | None = Field(default=None, repr=False) + jina_api_key: str | None = Field(default=None, repr=False) + deepseek_api_key: str | None = Field(default=None, repr=False) + grok_api_key: str | None = Field(default=None, repr=False) + lm_studio_api_key: str | None = Field(default=None, repr=False) + + _secret_fields: ClassVar[frozenset[str]] = frozenset( + { + "anthropic_api_key", "google_api_key", "siliconflow_api_key", + "gemini_api_key", + "google_application_credentials", + "openai_api_key", "qwen_api_key", "ark_api_key", "volc_access_key", + "volc_secret_key", "jina_api_key", "deepseek_api_key", "grok_api_key", + "lm_studio_api_key", + } + ) + + def _safe_dump_exclude(self, exclude: Any, include_secrets: bool) -> Any: + if include_secrets: + return exclude + secret_exclude = set(self._secret_fields) + if exclude is None: + return secret_exclude + if isinstance(exclude, Mapping): + return {**exclude, **{name: True for name in secret_exclude}} + return set(exclude) | secret_exclude + + def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + include_secrets = bool(kwargs.pop("include_secrets", False)) + exclude = self._safe_dump_exclude(kwargs.pop("exclude", None), include_secrets) + if exclude is not None: + kwargs["exclude"] = exclude + return super().model_dump(*args, **kwargs) + + def model_dump_json(self, *args: Any, **kwargs: Any) -> str: + include_secrets = bool(kwargs.pop("include_secrets", False)) + exclude = self._safe_dump_exclude(kwargs.pop("exclude", None), include_secrets) + if exclude is not None: + kwargs["exclude"] = exclude + return super().model_dump_json(*args, **kwargs) # --- [BASE_URLS] --- openai_api_base: str = "https://api.openai.com/v1" siliconflow_base_url: str = "https://api.siliconflow.cn/v1" - qwen_base_url: str = "https://dashscope.aliyuncs.com/api/v1" + qwen_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1" deepseek_base_url: str = "https://api.deepseek.com" ollama_base_url: str = "http://localhost:11434/v1" lm_studio_base_url: str = "http://localhost:1234/v1" - volc_base_url: str = "https://maas-api.ml-platform-cn-beijing.volces.com" + volc_base_url: str = "https://ark.cn-beijing.volces.com/api/v3" grok_base_url: str = "https://api.x.ai/v1" # --- [GENERAL] --- @@ -92,7 +212,7 @@ class Settings(BaseSettings): kb_remove_spaces: bool = False kb_remove_urls: bool = False kb_use_qa_segmentation: bool = False - kb_splitter_separators: List[str] = Field(default_factory=lambda: ["###"]) + kb_splitter_separators: list[str] = Field(default_factory=lambda: ["###"]) kb_chunk_size: int = 1500 kb_chunk_overlap: int = 150 kb_child_chunk_size: int = 300 @@ -117,17 +237,16 @@ class Settings(BaseSettings): chat_temperature: float = 0.7 # 将 chat_temperature 移到这里 # --- [MODEL_CONFIGURATIONS] --- - embedding_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { + embedding_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { "local-hash": ModelDetail(provider="local-hash", model_name="local-hash-256"), "google": ModelDetail(provider="google", model_name="embedding-001"), - "jina": ModelDetail(provider="jina", model_name="jina-embeddings-v2-base-zh"), "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-large-zh-v1.5"), "openai": ModelDetail(provider="openai", model_name="text-embedding-3-small"), }) - rerank_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { + rerank_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-reranker-large"), }) - llm_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { + llm_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { "google": ModelDetail(provider="google", model_name="gemini-1.5-pro-latest"), "anthropic": ModelDetail(provider="anthropic", model_name="claude-3-opus-20240229"), "qwen": ModelDetail(provider="qwen", model_name="qwen-turbo"), @@ -136,12 +255,25 @@ class Settings(BaseSettings): "volcengine": ModelDetail(provider="volcengine", model_name="Doubao-pro-32k"), "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V2-Chat"), "openai": ModelDetail(provider="openai", model_name="gpt-4o"), - "iflow-qwen3-max": ModelDetail(provider="openai", model_name="qwen3-max"), "ollama": ModelDetail(provider="ollama", model_name="llama3"), "lm-studio": ModelDetail(provider="lm-studio", model_name="LM-Studio-Community/Meta-Llama-3-8B-Instruct-GGUF"), }) # --- [VALIDATORS] --- + @field_validator("chat_top_k") + @classmethod + def validate_chat_top_k(cls, value: int) -> int: + if isinstance(value, bool) or value < 1: + raise ValueError("chat_top_k 必须是大于等于 1 的整数。") + return value + + @field_validator("chat_score_threshold") + @classmethod + def validate_chat_score_threshold(cls, value: float) -> float: + if isinstance(value, bool) or not 0 <= value <= 1: + raise ValueError("chat_score_threshold 必须在 0 到 1 之间。") + return value + @field_validator('log_level', mode='before') @classmethod def validate_log_level(cls, v: str) -> str: @@ -151,14 +283,33 @@ def validate_log_level(cls, v: str) -> str: raise ValueError(f"无效的日志级别: {v}. 必须是 {', '.join(valid_levels)} 中的一个。") return v.upper() + @field_validator("embedding_configurations", "rerank_configurations") + @classmethod + def validate_non_llm_protocol( + cls, + value: dict[str, ModelDetail], + info: ValidationInfo, + ) -> dict[str, ModelDetail]: + """Embedding/Rerank 配置不允许携带 LLM 线协议。""" + invalid = sorted( + key for key, detail in value.items() if detail.protocol is not None + ) + if invalid: + role = "Embedding" if info.field_name == "embedding_configurations" else "Rerank" + raise ValueError( + f"{role} 配置不支持 protocol;协议只能配置在 llm_configurations 中。" + f" 无效条目: {', '.join(invalid)}" + ) + return value + @field_validator('chat_temperature', mode='before') @classmethod def validate_chat_temperature(cls, v: Any) -> float: """验证聊天温度在 0.0 到 1.0 之间。""" try: value = float(v) - except (ValueError, TypeError): - raise ValueError(f"无法将聊天温度 '{v}' 转换为数字。") + except (ValueError, TypeError) as exc: + raise ValueError(f"无法将聊天温度 '{v}' 转换为数字。") from exc if not (0.0 <= value <= 1.0): raise ValueError(f"聊天温度必须在 0.0 到 1.0 之间,但得到 {value}。") @@ -183,8 +334,8 @@ def validate_retrieval_candidate_multiplier(cls, v: Any) -> int: """验证检索候选过量招募倍率。""" try: value = int(v) - except (ValueError, TypeError): - raise ValueError(f"无法将检索候选倍率 '{v}' 转换为整数。") + except (ValueError, TypeError) as exc: + raise ValueError(f"无法将检索候选倍率 '{v}' 转换为整数。") from exc if value < 1: raise ValueError(f"检索候选倍率必须大于等于 1,但得到 {value}。") @@ -192,7 +343,7 @@ def validate_retrieval_candidate_multiplier(cls, v: Any) -> int: @field_validator('kb_splitter_separators', mode='before') @classmethod - def split_separators(cls, v: Any) -> List[str]: + def split_separators(cls, v: Any) -> list[str]: """ 如果分隔符是字符串,则按逗号分割成列表。 如果输入值为空(None或空字符串),或者分割后为空列表,则使用字段的默认值。 @@ -200,7 +351,7 @@ def split_separators(cls, v: Any) -> List[str]: field_info = cls.model_fields["kb_splitter_separators"] default_factory = field_info.default_factory if default_factory is not None: - default_value = default_factory() + default_value = cast(Callable[[], Any], default_factory)() else: default_value = field_info.default if default_value is None: @@ -253,7 +404,7 @@ def settings_customise_sources( env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: """自定义配置加载源,保留环境变量、.env 和 config.toml 三层来源。""" return ( init_settings, @@ -271,7 +422,7 @@ def settings_customise_sources( protected_namespaces=(), ) -def load_toml_config() -> Dict[str, Any]: +def load_toml_config() -> dict[str, Any]: """ 从全局 CONFIG_TOML_PATH 路径加载 config.toml 文件配置。 """ @@ -286,7 +437,7 @@ def normalize_mapping(value: Any, preserve_keys: set[str] | None = None) -> Any: return value preserved = preserve_keys or set() - normalized: Dict[str, Any] = {} + normalized: dict[str, Any] = {} for raw_key, raw_value in value.items(): key = str(raw_key).lower() if key in preserved and isinstance(raw_value, dict): @@ -315,13 +466,21 @@ def normalize_mapping(value: Any, preserve_keys: set[str] | None = None) -> Any: preserve_keys={"embedding_configurations", "rerank_configurations", "llm_configurations"}, ) - flat_config: Dict[str, Any] = {} + flat_config: dict[str, Any] = {} scalar_keys = { "anthropic_api_key", "google_api_key", + "gemini_api_key", + # Vertex/Enterprise endpoint selection and project routing are not + # credentials, so they may be supplied by config.toml as well as env. + "google_genai_use_vertexai", + "google_genai_use_enterprise", + "google_cloud_project", + "google_cloud_location", "siliconflow_api_key", "openai_api_key", "qwen_api_key", + "ark_api_key", "volc_access_key", "volc_secret_key", "jina_api_key", @@ -400,7 +559,7 @@ def __call__(self) -> dict[str, Any]: # 3. 实例化并导出 (INSTANTIATE & EXPORT) # ================================================================= -@functools.lru_cache() +@functools.lru_cache def get_settings() -> Settings: """ 获取 Settings 实例的单例。 @@ -419,7 +578,7 @@ def get_settings() -> Settings: # 策略: 保持旧的配置变量,但使其从新的settings实例派生。 # 后续重构中,应逐步淘汰这些变量,直接使用 `get_settings()` 对象。 -def get_backward_compatible_configs() -> Dict[str, Any]: +def get_backward_compatible_configs() -> dict[str, Any]: """ 获取向后兼容的配置字典。 """ @@ -470,9 +629,11 @@ def get_backward_compatible_configs() -> Dict[str, Any]: API_CONFIG = { "ANTHROPIC_API_KEY": current_settings.anthropic_api_key, "GOOGLE_API_KEY": current_settings.google_api_key, + "GEMINI_API_KEY": current_settings.gemini_api_key, "SILICONFLOW_API_KEY": current_settings.siliconflow_api_key, "OPENAI_API_KEY": current_settings.openai_api_key, "QWEN_API_KEY": current_settings.qwen_api_key, + "ARK_API_KEY": current_settings.ark_api_key, "VOLC_ACCESS_KEY": current_settings.volc_access_key, "VOLC_SECRET_KEY": current_settings.volc_secret_key, "JINA_API_KEY": current_settings.jina_api_key, From 07cb1b566c3e650ae15b58620e0175737926733a Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:19 +0800 Subject: [PATCH 05/35] feat: adapt provider protocols and SDK surfaces Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/anthropic.py | 1822 +++++++++- src/providers/deepseek.py | 11 +- src/providers/google.py | 3825 +++++++++++++++++++- src/providers/grok.py | 7 +- src/providers/jina.py | 168 +- src/providers/lm_studio.py | 7 +- src/providers/local_hash.py | 24 +- src/providers/ollama.py | 7 +- src/providers/openai.py | 193 +- src/providers/openai_compatible.py | 5140 ++++++++++++++++++++++++++- src/providers/qwen.py | 7 +- src/providers/siliconflow.py | 7 +- src/providers/siliconflow_rerank.py | 168 +- src/providers/volcengine.py | 2431 ++++++++++++- 14 files changed, 13264 insertions(+), 553 deletions(-) diff --git a/src/providers/anthropic.py b/src/providers/anthropic.py index e1efddd..e592f76 100644 --- a/src/providers/anthropic.py +++ b/src/providers/anthropic.py @@ -1,13 +1,45 @@ -# -*- coding: utf-8 -*- +import base64 +import binascii +import inspect +import json +import re import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from collections.abc import AsyncGenerator, Generator, Mapping +from typing import Any import anthropic -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type -from src.providers.__base__.model_provider import LargeLanguageModel +from src.providers.__base__.model_provider import ( + CompletionRequest, + CompletionResult, + LargeLanguageModel, + StreamEvent, + close_resource_sync, + coerce_completion_request, + content_to_text, + extract_system_prompt, + field, + iterate_async, + merge_tool_call_fragment, + normalize_messages, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream_context, + retry_sync_call, + retry_sync_stream_context, + validate_secret_free_options, + validate_secret_free_request_overrides, +) +from src.providers.resources import AnthropicResources, AsyncAnthropicResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_resource_kwargs, +) logger = get_module_logger(__name__) @@ -17,8 +49,32 @@ class AnthropicProvider(LargeLanguageModel): 已增加 tenacity 重试机制与性能传感器。 """ - def __init__(self, model_name: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "token_count", "batches", "files", "models", "parse", + } + ) + + _MODEL_VERSION_RE = re.compile( + r"claude[-_]" + r"(?:" + r"(?:opus|sonnet|haiku)[-_](?P\d+)" + r"(?:[-_.](?P\d{1,2}))?" + r"(?:[-_]\d{8})?" + r"|" + r"(?P\d+)" + r"(?:[-_.](?P\d{1,2}))?" + r"[-_](?:opus|sonnet|haiku)" + r"(?:[-_]\d{8})?" + r")" + r"(?=$|[-_.])", + re.IGNORECASE, + ) + + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + self._options = validate_secret_free_options(options, "Anthropic") settings = get_settings() self._api_key = settings.anthropic_api_key @@ -26,104 +82,1750 @@ def __init__(self, model_name: str): logger.error("Anthropic API Key 未设置。") raise ValueError("ANTHROPIC_API_KEY is required for AnthropicProvider") - self._client: Optional[anthropic.Anthropic] = None - self._aclient: Optional[anthropic.AsyncAnthropic] = None + self._client: anthropic.Anthropic | None = None + self._aclient: anthropic.AsyncAnthropic | None = None logger.info(f"初始化 AnthropicProvider,模型: {model_name}") + @property + def resources(self) -> AnthropicResources: + return AnthropicResources(self) + + @property + def async_resources(self) -> AsyncAnthropicResources: + return AsyncAnthropicResources(self) + def _get_client(self) -> anthropic.Anthropic: if self._client is None: - self._client = anthropic.Anthropic(api_key=self._api_key) + validate_secret_free_options(self._options, "Anthropic") + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._client = anthropic.Anthropic(api_key=self._api_key, **client_options) return self._client def _get_aclient(self) -> anthropic.AsyncAnthropic: if self._aclient is None: - self._aclient = anthropic.AsyncAnthropic(api_key=self._api_key) + validate_secret_free_options(self._options, "Anthropic") + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._aclient = anthropic.AsyncAnthropic(api_key=self._api_key, **client_options) return self._aclient - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容异步客户端返回的协程和异步分页器。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _safe_resource_kwargs(kwargs: Mapping[str, Any], operation: str) -> dict[str, Any]: + """校验直连资源方法的请求扩展,避免绕过 Resource Facade。""" + return validate_secret_free_resource_kwargs(kwargs, f"Anthropic {operation}") + + @classmethod + def _sampling_controls_deprecated(cls, model_name: str) -> bool: + """识别 Claude 4.5+ 模型对采样控制字段的弃用契约。""" + match = cls._MODEL_VERSION_RE.search(str(model_name)) + if match is None: + return False + major = int(match.group("family_major") or match.group("version_major")) + minor_value = match.group("family_minor") or match.group("version_minor") + minor = int(minor_value) if minor_value is not None else 0 + return major > 4 or (major == 4 and minor >= 5) + + def supports_sampling_option(self, name: str) -> bool: + """供上层判断当前模型是否仍接受 temperature/top_p/top_k。""" + if name not in {"temperature", "top_p", "top_k"}: + raise ValueError(f"Anthropic 不支持的采样字段: {name}") + return ( + not self._sampling_controls_deprecated(self._model_name) + or getattr(self, "_options", {}).get("allow_deprecated_sampling") is True + ) + + def _normalize_sampling_value(self, name: str, value: Any) -> Any: + if value is None or not self._sampling_controls_deprecated(self._model_name): + return value + # Official Claude 4.5+ endpoints reject legacy sampling controls, but + # deployments behind an Anthropic-compatible gateway may intentionally + # retain them. Keep the strict default and require an explicit model + # option before forwarding those values. + if getattr(self, "_options", {}).get("allow_deprecated_sampling") is True: + return value + if name == "temperature": + try: + numeric = float(value) + except (TypeError, ValueError) as exc: + raise ValueError("Anthropic temperature 必须是数字。") from exc + if numeric in {0.7, 1.0}: + # 0.7 is the application's historical default; 1.0 is the + # backwards-compatible server value. Omitting either avoids a + # request field that modern Claude models no longer use. + return None + raise ValueError( + "当前 Claude 模型不支持该 temperature;请省略该字段或使用 1.0。" + ) + if name == "top_p": + try: + numeric = float(value) + except (TypeError, ValueError) as exc: + raise ValueError("Anthropic top_p 必须是数字。") from exc + if numeric >= 0.99: + return None + raise ValueError( + "当前 Claude 模型不支持该 top_p;仅允许省略或使用不低于 0.99 的兼容值。" + ) + raise ValueError("当前 Claude 模型不支持 top_k;请移除该字段。") + + def _default_max_tokens(self) -> int: + configured = getattr(self, "_options", {}).get("max_tokens") + if configured is not None: + if isinstance(configured, bool) or not isinstance(configured, (int, float)): + raise ValueError("Anthropic options.max_tokens 必须是正整数。") + configured_int = int(configured) + if configured_int <= 0 or configured_int != configured: + raise ValueError("Anthropic options.max_tokens 必须是正整数。") + return configured_int + model = str(getattr(self, "_model_name", "")).lower() + if "claude-3-opus" in model or "claude-3-haiku" in model: + return 4096 + if "claude-3-5" in model or "claude-3.5" in model: + return 8192 + if self._sampling_controls_deprecated(model): + return 16384 + return 8192 + + @staticmethod + def _user_profile_fields( + user: str | None, + extra_headers: Mapping[str, str] | None = None, + betas: Any = None, + *, + beta: bool, + ) -> tuple[dict[str, str] | None, Any]: + """补齐 user profile 所需的 SDK beta 资源声明。""" + if user is None: + return dict(extra_headers) if extra_headers is not None else None, betas + header = "user-profiles-2026-08-18" + if beta: + if isinstance(betas, str): + values = [betas] + else: + values = list(betas or []) + if header not in values: + values.append(header) + return dict(extra_headers) if extra_headers is not None else None, values + headers = dict(extra_headers or {}) + existing = headers.get("anthropic-beta", "") + values = [item.strip() for item in existing.split(",") if item.strip()] + if header not in values: + values.append(header) + headers["anthropic-beta"] = ",".join(values) + return headers, betas + + @staticmethod + def _convert_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + if not tools: + return None + + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Anthropic 工具定义[{index}] 必须是对象。") + tool_type = tool.get("type") + # Anthropic 的 server_* 工具不是 function schema,原样传递其 + # 专属字段,避免统一 OpenAI schema 破坏服务端工具配置。 + if isinstance(tool_type, str) and ( + tool_type.startswith("server_") or tool_type in { + "computer_20250124", "bash_20250124", "text_editor_20250124", + } + ): + converted.append(dict(tool)) + continue + function = tool.get("function", tool) + if not isinstance(function, Mapping): + raise ValueError( + f"Anthropic 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"Anthropic 工具定义[{index}] 缺少 function.name。") + description = function.get("description", "") + if description is None: + description = "" + if not isinstance(description, str): + raise ValueError( + f"Anthropic 工具定义[{index}].description 必须是字符串。" + ) + input_schema = function.get( + "parameters", + function.get("input_schema", {"type": "object", "properties": {}}), + ) + if not isinstance(input_schema, Mapping): + raise ValueError( + f"Anthropic 工具定义[{index}] 的 parameters/input_schema 必须是对象。" + ) + converted.append( + { + "name": name, + "description": description, + "input_schema": dict(input_schema), + } + ) + return converted + + @staticmethod + def _convert_tool_choice(tool_choice: Any) -> Any: + if tool_choice is None: + return None + if isinstance(tool_choice, str): + normalized = tool_choice.strip().lower() + if normalized in {"auto", "any", "none"}: + return {"type": normalized} + if normalized in {"required", "force"}: + return {"type": "any"} + raise ValueError(f"Anthropic 不支持 tool_choice: {tool_choice}") + if not isinstance(tool_choice, Mapping): + raise ValueError("Anthropic tool_choice 必须是字符串或对象。") + choice = dict(tool_choice) + choice_type = choice.get("type") + if choice_type in {"auto", "any", "none"}: + return choice + if choice_type == "tool": + name = choice.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Anthropic tool_choice 的 tool 缺少 name。") + return choice + if choice_type == "function": + function = choice.get("function", choice) + if not isinstance(function, Mapping): + raise ValueError("Anthropic tool_choice.function 必须是对象。") + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Anthropic tool_choice 的 function 缺少 name。") + return {"type": "tool", "name": name} + if choice_type is None and "name" in choice: + name = choice.get("name") + if isinstance(name, str) and name.strip(): + return {"type": "tool", "name": name} + raise ValueError( + "Anthropic tool_choice 必须是 auto、any、none、tool 或 function 对象。" + ) + + @staticmethod + def _convert_content(content: Any) -> Any: + if isinstance(content, str): + return content + if not isinstance(content, list): + return content + converted = [] + for part in content: + if not isinstance(part, dict): + converted.append(part) + continue + part_type = part.get("type") + if part_type in {"text", "input_text"}: + converted.append({"type": "text", "text": part.get("text", "")}) + elif part_type in {"image_url", "input_image"}: + image_url = part.get("image_url", part.get("url")) + if isinstance(image_url, Mapping): + image_url = image_url.get("url", image_url.get("image_url")) + if not isinstance(image_url, str) or not image_url.strip(): + raise ValueError("Anthropic 图片内容缺少有效 image_url。") + image_url = image_url.strip() + if image_url.startswith("data:"): + if "," not in image_url: + raise ValueError("Anthropic 图片 data URI 缺少逗号分隔符。") + header, data = image_url.split(",", 1) + header_parts = header.split(";", 1) + media_type = header_parts[0].removeprefix("data:") + if not media_type or len(header_parts) == 1 or header_parts[1].lower() != "base64": + raise ValueError( + "Anthropic 图片 data URI 必须包含 MIME 类型和 base64 标记。" + ) + try: + base64.b64decode(data, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError("Anthropic 图片 data URI 不是有效 Base64。") from exc + converted.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": data, + }, + } + ) + else: + converted.append( + {"type": "image", "source": {"type": "url", "url": image_url}} + ) + elif part_type == "tool_result": + converted.append(part) + else: + converted.append(part) + return converted + + @classmethod + def _convert_messages(cls, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + converted: list[dict[str, Any]] = [] + pending_tool_results: list[dict[str, Any]] = [] + + def flush_tool_results() -> None: + if pending_tool_results: + converted.append({"role": "user", "content": list(pending_tool_results)}) + pending_tool_results.clear() + + for message in messages: + role = message.get("role", "user") + content = message.get("content", "") + if role == "system": + continue + if role == "tool": + tool_call_id = message.get("tool_call_id") + if not isinstance(tool_call_id, str) or not tool_call_id.strip(): + raise ValueError( + "Anthropic 工具结果缺少 tool_call_id,无法关联对应的 tool_use。" + ) + tool_content = content + if not isinstance(tool_content, (str, list)): + tool_content = content_to_text(tool_content) + tool_result: dict[str, Any] = { + "type": "tool_result", + "tool_use_id": tool_call_id, + "content": tool_content, + } + if message.get("is_error") is not None: + tool_result["is_error"] = bool(message["is_error"]) + pending_tool_results.append(tool_result) + continue + + flush_tool_results() + if role == "assistant" and message.get("tool_calls"): + blocks: list[Any] = [] + if content: + blocks.extend(cls._convert_content(content) if isinstance(content, list) else [{"type": "text", "text": content}]) + for index, call in enumerate(message.get("tool_calls", [])): + if not isinstance(call, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 必须是对象。" + ) + function = call.get("function", call) + if not isinstance(function, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}].function 必须是对象。" + ) + call_id = call.get("id") + name = function.get("name") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 缺少 id。" + ) + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 缺少 function.name。" + ) + arguments = function.get("arguments", function.get("input", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 的 arguments 必须是 JSON 对象。" + ) + blocks.append({ + "type": "tool_use", + "id": call_id, + "name": name, + "input": dict(arguments), + }) + content = blocks + converted.append({"role": "assistant" if role == "assistant" else "user", "content": cls._convert_content(content)}) + flush_tool_results() + return converted + + @staticmethod + def _response_format_config(response_format: dict[str, Any] | None) -> dict[str, Any] | None: + if not response_format: + return None + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + return {"type": "json_schema", "schema": schema.get("schema", schema)} + if response_format.get("type") == "json_object": + return {"type": "json_schema", "schema": {"type": "object"}} + return response_format + + def _build_message_params( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + *, + stream: bool | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + cache_control: dict[str, Any] | None = None, + container: str | dict[str, Any] | None = None, + inference_geo: str | None = None, + output_config: dict[str, Any] | None = None, + output_format: dict[str, Any] | None = None, + beta: bool = False, + for_parse: bool = False, + for_tool_runner: bool = False, + context_management: dict[str, Any] | None = None, + mcp_servers: Any = None, + speed: str | None = None, + betas: Any = None, + diagnostics: dict[str, Any] | None = None, + fallback_credit_token: str | None = None, + fallbacks: Any = None, + **ignored: Any, + ) -> dict[str, Any]: + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Anthropic Messages", + ) + beta_values = { + "context_management": context_management, + "mcp_servers": mcp_servers, + "speed": speed, + "betas": betas, + "diagnostics": diagnostics, + "fallback_credit_token": fallback_credit_token, + "fallbacks": fallbacks, + } + if seed is not None: + raise ValueError("Anthropic Messages 不支持请求参数: seed") + if not beta: + ignored = { + **ignored, + **{key: value for key, value in beta_values.items() if value is not None}, + } + reject_unsupported_kwargs("Anthropic Messages", ignored) + normalized = normalize_messages(prompt, system_prompt, messages) + params: dict[str, Any] = { + "model": self._model_name, + "max_tokens": max_tokens if max_tokens is not None else self._default_max_tokens(), + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(messages) or system_prompt + if effective_system: + params["system"] = effective_system + converted_tools = self._convert_tools(tools) + if converted_tools: + params["tools"] = converted_tools + if stop is not None: + params["stop_sequences"] = [stop] if isinstance(stop, str) else stop + if tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(tool_choice) + if metadata is not None: + params["metadata"] = metadata + if cache_control is not None: + params["cache_control"] = cache_control + if container is not None: + params["container"] = container + if inference_geo is not None: + params["inference_geo"] = inference_geo + if thinking is not None: + params["thinking"] = thinking + if service_tier is not None: + params["service_tier"] = service_tier + if user is not None: + if for_parse and not beta: + raise ValueError( + "Anthropic Messages parse 不支持 user;请改用 beta_parse。" + ) + # The SDK translates this argument into the required header. Add + # the beta opt-in explicitly because ordinary Messages calls do + # not otherwise advertise user-profile support. + params["user_profile_id"] = user + extra_headers, configured_betas = self._user_profile_fields( + user, extra_headers, beta_values.get("betas"), beta=beta + ) + if beta: + beta_values["betas"] = configured_betas + if timeout is not None: + params["timeout"] = timeout + if extra_headers is not None: + params["extra_headers"] = extra_headers + if extra_query is not None: + params["extra_query"] = extra_query + # Client construction options must never leak into messages.create. + # Anthropic's SDK validates request kwargs locally and would otherwise + # report a misleading unknown parameter error. + # Keep this defensive filter for callers that construct a Provider + # through ``object.__new__`` or mutate an existing options mapping + # after initialization. Normal construction rejects these fields at + # the configuration boundary, but they must never reach messages.create. + client_only = { + "base_url", "timeout", "max_retries", "default_headers", + "default_query", "http_client", + } + options = { + key: value + for key, value in getattr(self, "_options", {}).items() + if key not in client_only + } + allow_deprecated_sampling = options.pop("allow_deprecated_sampling", False) + if not isinstance(allow_deprecated_sampling, bool): + raise ValueError("Anthropic options.allow_deprecated_sampling 必须是布尔值。") + configured_response_format = options.get("response_format") + if configured_response_format is not None: + if not isinstance(configured_response_format, Mapping): + raise ValueError("Anthropic options.response_format 必须是对象。") + if response_format is not None: + raise ValueError("Anthropic response_format 与配置项重复。") + response_format = dict(configured_response_format) + options.pop("response_format", None) + configured_output_format = options.pop("output_format", None) + if for_parse: + configured_output_format = None + if configured_output_format is not None: + if output_format is not None: + raise ValueError("Anthropic output_format 与配置项重复。") + output_format = configured_output_format + format_config = self._response_format_config(response_format) + if format_config: + params["output_config"] = {"format": format_config} + if output_config is not None: + params["output_config"] = output_config + if not for_parse: + if output_format is not None and stream is not True and not (beta and for_tool_runner): + raise ValueError( + "Anthropic Messages.create 不支持 output_format;请使用 parse 或 stream。" + ) + if output_format is not None and (stream is True or (beta and for_tool_runner)): + if isinstance(output_format, Mapping): + raise ValueError( + "Anthropic output_format 必须是 Python 类型;字典 schema 请放入 output_config。" + ) + params["output_format"] = output_format + if beta: + params.update({key: value for key, value in beta_values.items() if value is not None}) + configured_body = validate_secret_free_payload( + options.pop("extra_body", None), + "Anthropic Messages", + "options.extra_body", + ) + request_body = validate_secret_free_payload( + extra_body, + "Anthropic Messages", + "extra_body", + ) + overlap = sorted(set(configured_body).intersection(request_body)) + if overlap: + raise ValueError(f"Anthropic extra_body 不允许覆盖配置字段: {', '.join(overlap)}") + + sampling_options = { + key: options.pop(key, None) + for key in ("temperature", "top_p", "top_k") + } + sampling_values = { + "temperature": ( + temperature + if temperature is not None + else sampling_options["temperature"] + if sampling_options["temperature"] is not None + else None + ), + "top_p": top_p, + "top_k": top_k, + } + sampling_keys = set(sampling_values) + for key in sampling_keys: + if key in configured_body or key in request_body: + if sampling_options[key] is not None or sampling_values[key] is not None: + raise ValueError(f"Anthropic 请求字段 {key} 与 extra_body 重复。") + continue + value = sampling_values[key] + if value is None: + value = sampling_options[key] + value = self._normalize_sampling_value(key, value) + if value is not None: + request_body[key] = value + + body = {**configured_body, **request_body} + if body: + reserved = set(params).intersection(body) + if reserved: + raise ValueError(f"Anthropic extra_body 不允许覆盖请求字段: {', '.join(sorted(reserved))}") + params["extra_body"] = body + for key in ( + "model", "messages", "system", "max_tokens", "tool_choice", "tools", + ): + options.pop(key, None) + options.pop("max_tokens", None) + option_aliases = { + "stop": "stop_sequences", + "user": "user_profile_id", + } + allowed_options = { + "metadata", "cache_control", "container", "inference_geo", "thinking", + "service_tier", "extra_headers", "extra_query", "output_config", + "stop_sequences", "user_profile_id", + } + if beta: + allowed_options.update( + { + "context_management", "mcp_servers", "speed", "betas", + "diagnostics", "fallback_credit_token", "fallbacks", + } + ) + allowed_options.update(option_aliases) + reject_unsupported_kwargs( + "Anthropic Messages options", + {key: value for key, value in options.items() if key not in allowed_options}, + ) + for key, value in options.items(): + params.setdefault(option_aliases.get(key, key), value) + return params + + @classmethod + def _extract_result(cls, response: Any) -> CompletionResult: + blocks = field(response, "content", []) or [] + text = "".join(field(block, "text", "") for block in blocks if field(block, "type") == "text") + reasoning = "".join( + field(block, "thinking", "") + for block in blocks + if field(block, "type") in {"thinking", "redacted_thinking"} + ) + tool_calls = [ + { + "id": field(block, "id"), + "type": "function", + "name": field(block, "name"), + "arguments": field(block, "input", {}), + } + for block in blocks + if field(block, "type") == "tool_use" + ] + usage_dict = normalize_usage(field(response, "usage")) + refusal = next( + ( + field(block, "reason", "") or field(block, "text", "") + for block in blocks + if field(block, "type") == "refusal" + ), + None, + ) + if not refusal: + stop_reason = field(response, "stop_reason") + stop_details = field(response, "stop_details") or field(response, "refusal_stop_details") + details_type = field(stop_details, "type") + if stop_reason == "refusal" or details_type == "refusal": + explanation = field(stop_details, "explanation") + category = field(stop_details, "category") + refusal = str(explanation or category or "Anthropic 请求被安全策略拒绝") + return CompletionResult( + text=text, + tool_calls=tool_calls, + usage=usage_dict, + finish_reason=field(response, "stop_reason"), + response_id=field(response, "id"), + reasoning=reasoning, + refusal=refusal, + raw=response, + ) + + @classmethod + def _raise_for_refusal(cls, response: Any) -> None: + """将 Anthropic 的拒答/安全拦截转换为显式异常。""" + result = cls._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + + @classmethod + def _raise_for_stream_snapshot_refusal(cls, response: Any) -> None: + """在文本流输出前检查 SDK 当前快照中的拒答。""" + try: + snapshot = response.current_message_snapshot + except (AttributeError, NotImplementedError): + return + cls._raise_for_refusal(snapshot) + + @classmethod + def _iter_invoke_stream_text(cls, response: Any) -> Generator[str, None, None]: + """消费消息流文本,并保留拒答事件的可观测性。 + + 官方 MessageStream 暴露 ``current_message_snapshot``,文本迭代完成后 + 从最终快照读取 stop_details;简单测试替身通常只提供 text_stream 或 + 原始事件迭代,因此分别兼容这两种形态。 + """ + stream_iter = getattr(response, "__iter__", None) + if callable(stream_iter): + saw_event = False + message_stop_seen = False + for event in response: + saw_event = True + message_stop_seen = message_stop_seen or field(event, "type") == "message_stop" + converted = cls._stream_event(event) + if converted is None: + continue + if converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {converted.refusal}") + if converted.type == "text_delta" and converted.text: + yield converted.text + if saw_event: + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + return + + if hasattr(type(response), "current_message_snapshot"): + cls._raise_for_stream_snapshot_refusal(response) + for text in response.text_stream: + cls._raise_for_stream_snapshot_refusal(response) + if text: + yield text + cls._raise_for_stream_snapshot_refusal(response) + return + + for text in response.text_stream: + if text: + yield text + + @classmethod + async def _aiter_invoke_stream_text(cls, response: Any) -> AsyncGenerator[str, None]: + """异步版本的消息流文本消费与拒答检查。""" + stream_iter = getattr(response, "__aiter__", None) + if callable(stream_iter): + saw_event = False + message_stop_seen = False + async for event in iterate_async(response): + saw_event = True + message_stop_seen = message_stop_seen or field(event, "type") == "message_stop" + converted = cls._stream_event(event) + if converted is None: + continue + if converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {converted.refusal}") + if converted.type == "text_delta" and converted.text: + yield converted.text + if saw_event: + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + return + + if hasattr(type(response), "current_message_snapshot"): + cls._raise_for_stream_snapshot_refusal(response) + async for text in iterate_async(response.text_stream): + cls._raise_for_stream_snapshot_refusal(response) + if text: + yield text + cls._raise_for_stream_snapshot_refusal(response) + return + + async for text in iterate_async(response.text_stream): + if text: + yield text + + @classmethod + def _stream_event(cls, event: Any, response_id: str | None = None) -> StreamEvent | None: + event_type = field(event, "type", "") + if event_type == "message_start": + message = field(event, "message") + message_id = field(message, "id") or response_id + usage = normalize_usage(field(message, "usage")) + return StreamEvent(type="start", usage=usage, response_id=message_id, raw=event) + if event_type == "content_block_start": + block = field(event, "content_block") + block_type = field(block, "type") + if block_type == "refusal" or field(block, "refusal"): + refusal = field(block, "refusal") or field(block, "text") or field(block, "reason") + return StreamEvent( + type="refusal_delta", + refusal=refusal if isinstance(refusal, str) else str(refusal or ""), + response_id=response_id, + raw=event, + ) + if block_type == "tool_use": + initial_input = field(block, "input", {}) + if isinstance(initial_input, Mapping): + initial_arguments = json.dumps(initial_input, ensure_ascii=False, separators=(",", ":")) if initial_input else "" + else: + initial_arguments = str(initial_input) if initial_input else "" + return StreamEvent( + type="tool_call_delta", + tool_call={ + "id": field(block, "id"), + "index": field(event, "index"), + "name": field(block, "name"), + "arguments": initial_arguments, + }, + response_id=response_id, + raw=event, + ) + if block_type in {"thinking", "redacted_thinking"}: + thinking = field(block, "thinking", "") + return StreamEvent( + type="reasoning_delta", + reasoning=thinking if isinstance(thinking, str) else "", + response_id=response_id, + raw=event, + ) + return None + if event_type == "content_block_delta": + delta = field(event, "delta") + delta_type = field(delta, "type") + if delta_type == "text_delta": + return StreamEvent(type="text_delta", text=field(delta, "text", ""), response_id=response_id, raw=event) + if delta_type == "thinking_delta": + return StreamEvent(type="reasoning_delta", reasoning=field(delta, "thinking", ""), response_id=response_id, raw=event) + if delta_type in {"refusal_delta", "refusal"}: + refusal = field(delta, "refusal") or field(delta, "text") or field(delta, "reason") + return StreamEvent( + type="refusal_delta", + refusal=refusal if isinstance(refusal, str) else str(refusal or ""), + response_id=response_id, + raw=event, + ) + if delta_type == "input_json_delta": + return StreamEvent( + type="tool_call_delta", + tool_call={ + "index": field(event, "index"), + "arguments": field(delta, "partial_json", ""), + }, + response_id=response_id, + raw=event, + ) + return None + if event_type == "message_delta": + usage = normalize_usage(field(event, "usage")) + delta = field(event, "delta") + reason = field(delta, "stop_reason") + stop_details = field(delta, "stop_details") or field(delta, "refusal_stop_details") + details_type = field(stop_details, "type") + if reason == "refusal" or details_type == "refusal": + explanation = field(stop_details, "explanation") + category = field(stop_details, "category") + refusal = str(explanation or category or "Anthropic 请求被安全策略拒绝") + return StreamEvent( + type="refusal_delta", + refusal=refusal, + response_id=response_id, + raw=event, + ) + if reason: + return StreamEvent( + type="finish", + usage=usage, + finish_reason=str(reason), + response_id=response_id, + raw=event, + ) + if usage: + return StreamEvent(type="usage", usage=usage, response_id=response_id, raw=event) + return None + if event_type == "message_stop": + return StreamEvent(type="finish", response_id=response_id, raw=event) + if event_type == "error" or event_type.endswith(".error"): + raise_for_stream_error_event(event, "Anthropic Messages") + message = cls._stream_error_message(event) + raise RuntimeError(redact_sensitive_text(message)) + return None + + @staticmethod + def _stream_error_message(event: Any) -> str: + """从 SDK 错误事件的嵌套对象中保留最具体的错误信息。""" + pending: list[Any] = [event] + visited: set[int] = set() + preferred_keys = ("message", "detail", "reason", "error", "body") + while pending: + current = pending.pop(0) + if current is None or id(current) in visited: + continue + visited.add(id(current)) + if isinstance(current, str) and current.strip(): + return current.strip() + if isinstance(current, Mapping): + for key in preferred_keys: + value = current.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + pending.extend(current.get(key) for key in preferred_keys if key in current) + continue + for key in preferred_keys: + value = field(current, key) + if isinstance(value, str) and value.strip(): + return value.strip() + if value is not None: + pending.append(value) + return "Anthropic Messages 流返回错误" + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Anthropic stream_events") + request = request.copy_with(stream=True) + params = self._build_message_params(**request.to_invoke_kwargs()) + if not request.stream: + result = self.complete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, response_id=result.response_id, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + return + response_id: str | None = None + finish_emitted = False + message_stop_seen = False + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + with retry_sync_stream_context( + lambda: self._get_client().messages.stream(**params) + ) as stream: + for event in stream: + if field(event, "type") == "message_stop": + message_stop_seen = True + converted = self._stream_event(event, response_id) + if converted: + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + elif converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=converted.raw, + ) + if converted.type == "finish": + if finish_emitted: + continue + finish_emitted = True + response_id = converted.response_id or response_id + yield converted + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Anthropic astream_events") + request = request.copy_with(stream=True) + params = self._build_message_params(**request.to_invoke_kwargs()) + if not request.stream: + result = await self.acomplete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, response_id=result.response_id, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + return + response_id: str | None = None + finish_emitted = False + message_stop_seen = False + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + async with retry_async_stream_context( + lambda: self._get_aclient().messages.stream(**params) + ) as stream: + async for event in iterate_async(stream): + if field(event, "type") == "message_stop": + message_stop_seen = True + converted = self._stream_event(event, response_id) + if converted: + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + elif converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=converted.raw, + ) + if converted.type == "finish": + if finish_emitted: + continue + finish_emitted = True + response_id = converted.response_id or response_id + yield converted + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 异步流在 message_stop 之前结束。") + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Anthropic complete") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, stream=False) + response = retry_sync_call(lambda: self._get_client().messages.create(**params)) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncAnthropic 聚合完整结果,保留工具、thinking 和 usage。""" + request = coerce_completion_request(request, kwargs, "Anthropic acomplete") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, stream=False) + response = await retry_async_call(lambda: self._get_aclient().messages.create(**params)) + return self._extract_result(response) + + def parse( + self, + output_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """调用 Anthropic Messages 的原生结构化输出解析入口。""" + if output_format is None: + raise ValueError("Anthropic parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params = self._parse_params(params, beta=False) + return self._get_client().messages.parse(output_format=output_format, **params) + + @staticmethod + def _parse_params(params: dict[str, Any], *, beta: bool) -> dict[str, Any]: + """只保留当前 SDK 的 Messages.parse 参数。""" + allowed = { + "model", "max_tokens", "messages", "metadata", "output_config", + "output_format", "service_tier", "stop_sequences", "system", + "thinking", "tool_choice", "tools", + "extra_headers", "extra_query", "extra_body", "timeout", + } + if beta: + allowed.update( + { + "cache_control", "container", "context_management", "diagnostics", + "fallback_credit_token", "fallbacks", "inference_geo", "mcp_servers", + "speed", "betas", "user_profile_id", + } + ) + reject_unsupported_kwargs( + "Anthropic Beta Messages parse" if beta else "Anthropic Messages parse", + {key: value for key, value in params.items() if key not in allowed}, + ) + return {key: value for key, value in params.items() if key in allowed} + + @staticmethod + def _validate_token_count_request(request: CompletionRequest, *, beta: bool) -> None: + """拒绝不会被当前 Messages count_tokens SDK 使用的请求字段。""" + allowed = { + "prompt", "system_prompt", "messages", "tools", "tool_choice", + "cache_control", "response_format", "output_config", + # CompletionRequest defaults temperature for generation. It has no + # effect on token counting, so accept the shared default without + # forwarding it to the count_tokens endpoint. + "thinking", "user", "extra_body", "extra_headers", "extra_query", + "timeout", "stream", "temperature", + } + if request.output_format is not None: + mode = "Beta " if beta else "" + raise ValueError( + f"Anthropic {mode}Messages token count 不支持 output_format;" + "请使用 output_config。" + ) + if beta: + allowed.update({"context_management", "mcp_servers", "speed", "betas"}) + reject_unsupported_kwargs( + "Anthropic Beta Messages token count" if beta else "Anthropic Messages token count", + { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in allowed + }, + ) + + def count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic count_tokens") + self._validate_token_count_request(request, beta=False) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + tools = self._convert_tools(request.tools) + if tools: + params["tools"] = tools + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.user is not None: + params["user_profile_id"] = request.user + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.thinking is not None: + params["thinking"] = request.thinking + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Messages token count", + "extra_body", + ) + profile_headers, _ = self._user_profile_fields( + request.user, request.extra_headers, beta=False + ) + if profile_headers is not None: + params["extra_headers"] = profile_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = self._get_client().messages.count_tokens(**params) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic token count 响应缺少 input_tokens。") + return int(value) + + def beta_create(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """调用 Anthropic Beta Messages,保留 Beta 专属参数转发能力。""" + request = coerce_completion_request(request, kwargs, "Anthropic beta_create") + if request.output_format is not None: + raise ValueError( + "Anthropic Beta Messages.create 不支持 output_format;请使用 beta_parse 或 beta_stream。" + ) + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False) + return self._get_client().beta.messages.create(**params) + + def beta_parse(self, output_format: Any, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + if output_format is None: + raise ValueError("Anthropic Beta parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic beta_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, beta=True, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params.pop("output_format", None) + params = self._parse_params(params, beta=True) + return self._get_client().beta.messages.parse(output_format=output_format, **params) + + def beta_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic beta_count_tokens") + self._validate_token_count_request(request, beta=True) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + if request.tools: + params["tools"] = self._convert_tools(request.tools) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.context_management is not None: + params["context_management"] = request.context_management + if request.mcp_servers is not None: + params["mcp_servers"] = request.mcp_servers + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.thinking is not None: + params["thinking"] = request.thinking + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.speed is not None: + params["speed"] = request.speed + if request.user is not None: + params["user_profile_id"] = request.user + _, profile_betas = self._user_profile_fields( + request.user, request.extra_headers, request.betas, beta=True + ) + if profile_betas is not None: + params["betas"] = profile_betas + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Beta Messages token count", + "extra_body", + ) + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = self._get_client().beta.messages.count_tokens(**params) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic Beta token count 响应缺少 input_tokens。") + return int(value) + + def beta_stream(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 Anthropic Beta Messages 流式上下文管理器。""" + request = coerce_completion_request(request, kwargs, "Anthropic beta_stream") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=True) + return retry_sync_stream_context( + lambda: self._get_client().beta.messages.stream(**params) + ) + + def beta_tool_runner( + self, tools: Any, request: CompletionRequest | None = None, **kwargs: Any + ) -> Any: + """创建 Anthropic Beta 工具运行器,保留 SDK 原生 runner 对象。""" + if "compaction_control" in kwargs: + raise ValueError( + "Anthropic Beta tool_runner 当前 SDK 不支持 compaction_control。" + ) + runner_options = { + key: kwargs.pop(key) + for key in ("max_iterations",) + if key in kwargs + } + request = coerce_completion_request(request, kwargs, "Anthropic beta_tool_runner") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("tools", None) + params = self._build_message_params( + **values, beta=True, stream=request.stream, for_tool_runner=True + ) + params["tools"] = tools + params["stream"] = request.stream + params.update(runner_options) + return self._get_client().beta.messages.tool_runner(**params) + + def create_batch(self, requests: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.create(requests=requests, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.retrieve(batch_id, **kwargs) + + def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.results(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.cancel(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.list(**kwargs) + + def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.delete(batch_id, **kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.upload(file=file, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.list(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.retrieve_metadata(file_id, **kwargs) + + def download_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.download(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.delete(file_id, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return self._get_client().models.list(**kwargs) + + def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return self._get_client().models.retrieve(model_id, **kwargs) + + async def async_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic async_count_tokens") + self._validate_token_count_request(request, beta=False) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + tools = self._convert_tools(request.tools) + if tools: + params["tools"] = tools + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.user is not None: + params["user_profile_id"] = request.user + if request.output_config is not None: + params["output_config"] = request.output_config + format_config = self._response_format_config(request.response_format) + if format_config and "output_config" not in params: + params["output_config"] = {"format": format_config} + if request.thinking is not None: + params["thinking"] = request.thinking + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Messages token count", + "extra_body", + ) + profile_headers, _ = self._user_profile_fields( + request.user, request.extra_headers, beta=False + ) + if profile_headers is not None: + params["extra_headers"] = profile_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = await self._resolve_async_result(self._get_aclient().messages.count_tokens(**params)) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic token count 响应缺少 input_tokens。") + return int(value) + + async def async_beta_create(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_create") + if request.output_format is not None: + raise ValueError( + "Anthropic Beta Messages.create 不支持 output_format;请使用 beta_parse 或 beta_stream。" + ) + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False) + return await self._resolve_async_result(self._get_aclient().beta.messages.create(**params)) + + async def async_beta_parse(self, output_format: Any, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + if output_format is None: + raise ValueError("Anthropic Beta parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params.pop("output_format", None) + params = self._parse_params(params, beta=True) + return await self._resolve_async_result( + self._get_aclient().beta.messages.parse(output_format=output_format, **params) + ) + + async def async_beta_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_count_tokens") + self._validate_token_count_request(request, beta=True) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + if request.tools: + params["tools"] = self._convert_tools(request.tools) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.context_management is not None: + params["context_management"] = request.context_management + if request.mcp_servers is not None: + params["mcp_servers"] = request.mcp_servers + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.thinking is not None: + params["thinking"] = request.thinking + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.speed is not None: + params["speed"] = request.speed + if request.user is not None: + params["user_profile_id"] = request.user + _, profile_betas = self._user_profile_fields( + request.user, request.extra_headers, request.betas, beta=True + ) + if profile_betas is not None: + params["betas"] = profile_betas + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Beta Messages token count", + "extra_body", + ) + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = await self._resolve_async_result( + self._get_aclient().beta.messages.count_tokens(**params) + ) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic Beta token count 响应缺少 input_tokens。") + return int(value) + + def async_beta_stream(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 AsyncAnthropic Beta 流式上下文管理器。""" + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_stream") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=True) + return retry_async_stream_context( + lambda: self._get_aclient().beta.messages.stream(**params) + ) + + def async_beta_tool_runner( + self, tools: Any, request: CompletionRequest | None = None, **kwargs: Any + ) -> Any: + """创建 AsyncAnthropic Beta 工具运行器。""" + if "compaction_control" in kwargs: + raise ValueError( + "Anthropic Beta tool_runner 当前 SDK 不支持 compaction_control。" + ) + runner_options = { + key: kwargs.pop(key) + for key in ("max_iterations",) + if key in kwargs + } + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_tool_runner") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("tools", None) + params = self._build_message_params( + **values, beta=True, stream=request.stream, for_tool_runner=True + ) + params["tools"] = tools + params["stream"] = request.stream + params.update(runner_options) + return self._get_aclient().beta.messages.tool_runner(**params) + + async def async_parse( + self, + output_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 Anthropic Messages 的原生结构化输出解析入口。""" + if output_format is None: + raise ValueError("Anthropic parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic async_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params = self._parse_params(params, beta=False) + return await self._resolve_async_result( + self._get_aclient().messages.parse(output_format=output_format, **params) + ) + + def close(self) -> None: + """释放同步和异步 Anthropic SDK 客户端。 + + 异步客户端在没有运行中事件循环时通过同步生命周期桥接器关闭; + 若当前已处于事件循环,则保留引用并显式要求调用 ``aclose``。 + """ + client = self._client + async_client = self._aclient + self._client = None + first_error: Exception | None = None + if client is not None: + try: + client.close() + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + try: + close_resource_sync(async_client, "Anthropic 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error + + async def async_create_batch(self, requests: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result( + self._get_aclient().messages.batches.create(requests=requests, **kwargs) + ) + + async def async_retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.retrieve(batch_id, **kwargs)) + + async def async_batch_results(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.results(batch_id, **kwargs)) + + async def async_cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.cancel(batch_id, **kwargs)) + + async def async_list_batches(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.list(**kwargs)) + + async def async_delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.delete(batch_id, **kwargs)) + + async def async_upload_file(self, file: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.upload(file=file, **kwargs)) + + async def async_list_files(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.list(**kwargs)) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.retrieve_metadata(file_id, **kwargs)) + + async def async_download_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.download(file_id, **kwargs)) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.delete(file_id, **kwargs)) + + async def async_list_models(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return await self._resolve_async_result(self._get_aclient().models.list(**kwargs)) + + async def async_retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return await self._resolve_async_result(self._get_aclient().models.retrieve(model_id, **kwargs)) + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> Generator[str, None, None]: """同步调用 Anthropic Claude LLM (CSE Sensor)。""" logger.info(f"调用 Anthropic LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - + try: - params = { - "model": self._model_name, - "max_tokens": 4096, - "messages": [{"role": "user", "content": prompt}], - "temperature": temperature, - } - if system_prompt: - params["system"] = system_prompt + params = self._build_message_params( + prompt, + system_prompt, + tools, + temperature, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + top_k=top_k, + stop=stop, + response_format=response_format, + tool_choice=tool_choice, + extra_body=extra_body, + metadata=metadata, + thinking=thinking, + service_tier=service_tier, + user=user, + timeout=timeout, + stream=stream, + **kwargs, + ) if stream: - with client.messages.stream(**params) as response: - for text in response.text_stream: - yield text + with retry_sync_stream_context( + lambda: client.messages.stream(**params) + ) as response: + yield from self._iter_invoke_stream_text(response) else: - response = client.messages.create(**params) - if response.content: - yield response.content[0].text - + response = retry_sync_call(lambda: client.messages.create(**params)) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + if result.text: + yield result.text + duration = time.perf_counter() - start_time logger.info(f"Anthropic LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Anthropic LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Anthropic LLM (%s) 出错: %s", + self._model_name, + error_text, + ) raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> AsyncGenerator[str, None]: """异步调用 Anthropic Claude LLM (CSE Sensor)。""" logger.info(f"异步调用 Anthropic LLM ({self._model_name}),流式: {stream}") aclient = self._get_aclient() start_time = time.perf_counter() - + try: - params = { - "model": self._model_name, - "max_tokens": 4096, - "messages": [{"role": "user", "content": prompt}], - "temperature": temperature, - } - if system_prompt: - params["system"] = system_prompt + params = self._build_message_params( + prompt, + system_prompt, + tools, + temperature, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + top_k=top_k, + stop=stop, + response_format=response_format, + tool_choice=tool_choice, + extra_body=extra_body, + metadata=metadata, + thinking=thinking, + service_tier=service_tier, + user=user, + timeout=timeout, + stream=stream, + **kwargs, + ) if stream: - async with aclient.messages.stream(**params) as response: - async for text in response.text_stream: + async with retry_async_stream_context( + lambda: aclient.messages.stream(**params) + ) as response: + async for text in self._aiter_invoke_stream_text(response): yield text else: - response = await aclient.messages.create(**params) - if response.content: - yield response.content[0].text - + response = await retry_async_call( + lambda: aclient.messages.create(**params) + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + if result.text: + yield result.text + duration = time.perf_counter() - start_time logger.info(f"Anthropic LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Anthropic LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Anthropic LLM (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/deepseek.py b/src/providers/deepseek.py index 5b2fa26..2829c45 100644 --- a/src/providers/deepseek.py +++ b/src/providers/deepseek.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,9 @@ class DeepSeekProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="deepseek") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="deepseek", protocol=protocol, options=options) + + def _chat_max_tokens_key(self) -> str: + """DeepSeek Chat Completions 仍使用旧版 max_tokens 字段。""" + return "max_tokens" diff --git a/src/providers/google.py b/src/providers/google.py index 9cf5ad5..4803f54 100644 --- a/src/providers/google.py +++ b/src/providers/google.py @@ -1,166 +1,3861 @@ -# -*- coding: utf-8 -*- +import base64 +import binascii +import inspect +import json +import math +import os +import re import time -from typing import Any, Dict, Generator, List, cast, AsyncGenerator +from collections.abc import AsyncGenerator, Generator, Mapping, Sequence +from enum import Enum +from typing import Any, ClassVar +from urllib.parse import parse_qsl, quote, urlsplit from google import genai +from google.auth.credentials import Credentials from google.genai import types -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + extract_system_prompt, + field, + is_retryable_error, + iterate_async, + normalize_embedding_vector, + normalize_messages, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, ) +from src.providers.resources import AsyncGoogleResources, GoogleResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + _model_dump_mapping, + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_request_overrides, +) logger = get_module_logger(__name__) + class GoogleProvider(LargeLanguageModel, TextEmbeddingModel): """ Google模型提供商,统一处理Gemini LLM和Embedding。 已升级为使用最新的 google-genai SDK (v1.0+)。 """ - def __init__(self, model_name: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "token_count", "embedding", "files", "batches", "cache", + "models", "tuning", "images", "videos", "file_search", + "interactions", "agents", "webhooks", "environments", "triggers", "live", + "auth_tokens", "operations", "chats", + } + ) + + _EXPERIMENTAL_RESOURCE_LABELS: ClassVar[dict[str, str]] = { + "auth_tokens": "Auth Token", + "interactions": "Interactions", + "agents": "Agents", + "webhooks": "Webhooks", + "environments": "Environments", + "triggers": "Triggers", + } + + _CLIENT_OPTION_KEYS = frozenset( + { + "enterprise", + "vertexai", + "project", + "location", + "credentials", + "http_options", + "debug_config", + } + ) + _OPTION_KEYS = _CLIENT_OPTION_KEYS | frozenset( + { + "extra_body", + "response_format", + "timeout", + "max_tokens", + "task_type", + "output_dimensionality", + "title", + "mime_type", + "auto_truncate", + "document_ocr", + "audio_track_extraction", + "temperature", + "top_p", + "top_k", + "candidate_count", + "max_output_tokens", + "stop_sequences", + "presence_penalty", + "frequency_penalty", + "seed", + "response_mime_type", + "response_schema", + "response_json_schema", + "safety_settings", + "cached_content", + "response_modalities", + "thinking", + "thinking_config", + "automatic_function_calling", + "speech_config", + "image_config", + "labels", + "routing_config", + "model_selection_config", + "media_resolution", + "enable_enhanced_civic_answers", + "service_tier", + "logprobs", + "response_logprobs", + "audio_timestamp", + "audio_transcription_config", + "model_armor_config", + "should_return_http_response", + } + ) + + _HTTP_OPTIONS_SENSITIVE_EXACT = frozenset( + { + "apikey", "xapikey", "accesskey", "secretkey", "token", "password", + "passwd", "authorization", "auth", "bearer", "accesstoken", + "xaccesstoken", "authtoken", "xauthtoken", "authkey", "credential", + "credentials", "cookie", "setcookie", "proxyauthorization", "secret", + "privatekey", "httpclient", "httpxclient", "httpxasyncclient", + "aiohttpclient", "clientargs", "asyncclientargs", "baseurl", + "baseurlresourcescope", "proxy", "proxies", "transport", "verify", + "cert", "mounts", + } + ) + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + if options is not None and not isinstance(options, Mapping): + raise ValueError("Google options 必须是对象。") + self._options = dict(options or {}) + self._validate_options(self._options) logger.info(f"初始化 GoogleProvider (google-genai),模型: {model_name}") - self._client: genai.Client | None = None + self._client: Any | None = None - def _get_client(self) -> genai.Client: + @property + def resources(self) -> GoogleResources: + return GoogleResources(self) + + @property + def async_resources(self) -> AsyncGoogleResources: + return AsyncGoogleResources(self) + + def _get_client(self) -> Any: """延迟初始化并返回 genai.Client 实例。""" - if not self._client: + # Some test doubles and SDK client wrappers define false-y equality or + # length semantics. Only ``None`` means that initialization has not + # happened yet; a valid but false-y client must be reused. + if self._client is None: + self._validate_options(self._options) settings = get_settings() - if not settings.google_api_key: + credentials = self._options.get("credentials") + vertexai = self._configured_vertex_mode() + if credentials is None and os.getenv("GOOGLE_APPLICATION_CREDENTIALS") is None: + credential_path = self._configured_setting( + settings, + "google_application_credentials", + "GOOGLE_APPLICATION_CREDENTIALS", + ) + if credential_path: + credentials = self._load_credentials_file(credential_path) + api_key = self._configured_api_key(settings) + if credentials is not None and vertexai is False: + raise ValueError( + "Google credentials 需要 Vertex/Enterprise 模式;" + "请将 vertexai 或 enterprise 设为 true。" + ) + if credentials is not None and vertexai is None: + vertexai = True + if credentials is None and not api_key and vertexai is not True: logger.error("Google API Key 未设置。") - raise ValueError("GOOGLE_API_KEY is required for GoogleProvider") + raise ValueError( + "GOOGLE_API_KEY is required for GoogleProvider unless options.credentials is provided " + "or Vertex/Enterprise mode enables Application Default Credentials" + ) logger.debug("正在初始化 google-genai Client") - self._client = genai.Client(api_key=settings.google_api_key) + client_options = { + key: value + for key, value in self._options.items() + if key in self._CLIENT_OPTION_KEYS + } + for key in ("enterprise", "vertexai"): + if key in client_options: + client_options[key] = self._as_bool(client_options[key]) + if "enterprise" not in client_options and "vertexai" not in client_options: + configured_flags = self._configured_environment_vertex_flags(settings) + if "GOOGLE_GENAI_USE_ENTERPRISE" in configured_flags: + client_options["enterprise"] = configured_flags[ + "GOOGLE_GENAI_USE_ENTERPRISE" + ] + elif "GOOGLE_GENAI_USE_VERTEXAI" in configured_flags: + client_options["vertexai"] = configured_flags[ + "GOOGLE_GENAI_USE_VERTEXAI" + ] + if "debug_config" in client_options: + client_options["debug_config"] = self._coerce_debug_config( + client_options["debug_config"] + ) + if credentials is not None: + if "credentials" not in client_options: + client_options["credentials"] = credentials + if vertexai is True and "enterprise" not in client_options and "vertexai" not in client_options: + # google-genai treats explicit credentials as Vertex ADC + # credentials only when the Vertex transport is selected. + # Make that implication explicit instead of letting the SDK + # fall through to Developer API key validation. + client_options["vertexai"] = True + if vertexai is True: + for option_name, setting_name, env_name in ( + ("project", "google_cloud_project", "GOOGLE_CLOUD_PROJECT"), + ("location", "google_cloud_location", "GOOGLE_CLOUD_LOCATION"), + ): + if option_name not in client_options: + configured_value = self._configured_setting( + settings, setting_name, env_name + ) + if configured_value: + client_options[option_name] = configured_value + # ``google-genai`` supports both ADC and Vertex express-mode API + # keys. Only explicit credentials and an API key are mutually + # exclusive; an API key remains valid when the Vertex endpoint is + # selected and must not be dropped when it came from config.toml. + if credentials is None and api_key: + self._client = genai.Client(api_key=api_key, **client_options) + else: + # google-genai can resolve Vertex ADC when neither api_key nor + # explicit credentials is supplied; credentials and api_key + # must never be passed together. + self._client = genai.Client(**client_options) logger.info("google-genai Client 初始化成功。") return self._client - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + @staticmethod + def _configured_api_key(settings: Any) -> str | None: + """读取 Google SDK 支持的两个环境键,优先 GOOGLE_API_KEY。""" + for name in ("google_api_key", "gemini_api_key"): + value = getattr(settings, name, None) + # MagicMock-style test doubles expose arbitrary attributes; only + # actual strings are valid SDK API keys here. + if isinstance(value, str) and value.strip(): + return value + return None + + @staticmethod + def _configured_setting(settings: Any, field_name: str, env_name: str) -> str | None: + """读取环境变量或 Settings 中的非空字符串配置。""" + value = os.getenv(env_name) + if value is None: + value = getattr(settings, field_name, None) + if isinstance(value, str) and value.strip(): + return value.strip() + return None + + @staticmethod + def _load_credentials_file(path: str) -> Any: + """从 Settings 中配置的服务账号路径加载 ADC 凭证。""" + try: + from google.auth import load_credentials_from_file + except ImportError as exc: # pragma: no cover - google-genai supplies google-auth + raise RuntimeError("当前环境缺少 google-auth,无法加载 Google ADC 凭证。") from exc + try: + credentials, _ = load_credentials_from_file(path) + except Exception as exc: + raise ValueError("Google GOOGLE_APPLICATION_CREDENTIALS 文件无法加载。") from exc + return credentials + + @staticmethod + def _as_bool(value: Any) -> bool: + """将配置文件中的布尔字符串规范为 SDK 需要的 bool。""" + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + @classmethod + def _validate_options(cls, options: Mapping[str, Any]) -> None: + if not isinstance(options, Mapping): + raise ValueError("Google options 必须是对象。") + unknown = sorted(set(options).difference(cls._OPTION_KEYS)) + if unknown: + raise ValueError( + "Google options 包含不支持的字段: " + ", ".join(unknown) + ) + if "extra_body" in options: + validate_secret_free_payload( + options["extra_body"], + "Google", + "options.extra_body", + ) + if "http_options" in options: + cls._validate_http_options(options["http_options"]) + if "timeout" in options: + cls._validate_timeout(options["timeout"], "Google options.timeout") + configured_body = options.get("extra_body") + model_conflicts: list[str] = [] + if options.get("http_options") is not None: + if options.get("timeout") is not None: + model_conflicts.append("timeout") + if configured_body: + model_conflicts.append("extra_body") + if model_conflicts: + raise ValueError( + "Google options.http_options 不能与模型级 options 同时配置: " + + ", ".join(model_conflicts) + ) + + @staticmethod + def _require_resource(client: Any, name: str, label: str) -> Any: + """读取 SDK 资源并把版本差异转换成明确的能力错误。""" + + try: + resource = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"当前 google-genai SDK 未提供 {label}资源;请升级 SDK 或使用官方支持的接口。" + ) from exc + if resource is None: + raise NotImplementedError( + f"当前 google-genai SDK 未提供 {label}资源;请升级 SDK 或使用官方支持的接口。" + ) + return resource + + def _require_provider_resource(self, method_name: str) -> None: + """把实验性资源的版本差异转换成明确的能力错误。""" + name = method_name.removeprefix("async_") + resource_name: str | None = None + for candidate in self._EXPERIMENTAL_RESOURCE_LABELS: + singular = candidate.removesuffix("s") + if name.startswith(singular) or f"_{singular}" in name: + resource_name = candidate + break + if resource_name is None: + if name == "create_auth_token": + resource_name = "auth_tokens" + else: + return + client = self._get_client() + if method_name.startswith("async_"): + try: + client = client.aio + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"当前 google-genai SDK 未提供异步 {self._EXPERIMENTAL_RESOURCE_LABELS[resource_name]}资源。" + ) from exc + self._require_resource( + client, + resource_name, + self._EXPERIMENTAL_RESOURCE_LABELS[resource_name], + ) + + @classmethod + def _validate_http_options(cls, value: Any) -> None: + """校验 Google SDK HTTP 扩展,允许普通追踪头但拒绝凭证。""" + if value is None: + return + if isinstance(value, Mapping): + payload: Any = dict(value) + else: + model_dump = getattr(value, "model_dump", None) + if not callable(model_dump): + raise ValueError( + "Google options.http_options 必须是对象表或 google-genai HttpOptions。" + ) + try: + payload = model_dump(exclude_none=True) + except TypeError: + try: + payload = model_dump() + except Exception as exc: + raise ValueError("Google options.http_options 无法读取。") from exc + except Exception as exc: + raise ValueError("Google options.http_options 无法读取。") from exc + if not isinstance(payload, Mapping): + raise ValueError("Google options.http_options 必须是对象。") + + if "timeout" in payload and payload["timeout"] is not None: + timeout = payload["timeout"] + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or float(timeout) <= 0 + or int(float(timeout)) != float(timeout) + ): + raise ValueError( + "Google options.http_options.timeout 必须是正整数毫秒。" + ) + + found: list[str] = [] + + def normalized_key(key: Any) -> str: + return "".join(char for char in str(key).lower() if char.isalnum()) + + def is_sensitive_key(key: Any) -> bool: + normalized = normalized_key(key) + if normalized in cls._HTTP_OPTIONS_SENSITIVE_EXACT: + return True + return any( + normalized.endswith(marker) + for marker in ( + "apikey", "accesskey", "accesstoken", "authtoken", + "secret", "secretkey", "clientsecret", "password", + "credential", "cookie", + ) + ) + + def check_url_query(value: Any, path: str) -> None: + if not isinstance(value, str): + return + try: + query = parse_qsl(urlsplit(value).query, keep_blank_values=True) + except ValueError: + return + for key, _ in query: + if is_sensitive_key(key): + found.append(f"{path}?{key}") + + def visit(item: Any, path: str = "") -> None: + if isinstance(item, Mapping): + for key, nested in item.items(): + current = f"{path}.{key}" if path else str(key) + if nested is None: + continue + if is_sensitive_key(key): + found.append(current) + continue + visit(nested, current) + elif isinstance(item, (list, tuple, set, frozenset)): + for index, nested in enumerate(item): + visit(nested, f"{path}[{index}]") + elif isinstance(item, str): + if redact_sensitive_text(item) != item: + found.append(path or "value") + check_url_query(item, path) + try: + parsed = urlsplit(item) + except ValueError: + parsed = None + if parsed is not None and (parsed.username is not None or parsed.password is not None): + found.append(f"{path} userinfo") + + visit(payload) + if found: + raise ValueError( + "Google options.http_options 不允许包含凭证、自定义客户端或连接覆盖: " + + ", ".join(sorted(found)) + ) + + @staticmethod + def _validate_timeout(value: Any, field_name: str) -> float: + """将请求 timeout 规范为有限正数,避免底层比较抛出 TypeError。""" + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(float(value)) + or float(value) <= 0 + ): + raise ValueError(f"{field_name} 必须是正数。") + return float(value) + + @classmethod + def _validate_resource_config(cls, config: Any) -> None: + """检查资源 config 中的 HTTP 配置,但不误判业务字段。""" + if config is None: + return + if isinstance(config, Mapping): + payload: Any = dict(config) + else: + payload = _model_dump_mapping(config) + if not isinstance(payload, Mapping): + return + + def visit(value: Any) -> None: + if isinstance(value, Mapping): + for key, nested in value.items(): + normalized = "".join( + char for char in str(key).lower() if char.isalnum() + ) + if normalized == "httpoptions": + cls._validate_http_options(nested) + continue + visit(nested) + return + dumped = _model_dump_mapping(value) + if dumped is not None: + visit(dumped) + elif isinstance(value, (list, tuple, set, frozenset)): + for nested in value: + visit(nested) + + visit(payload) + + @staticmethod + def _coerce_debug_config(value: Any) -> Any: + """将配置文件中的 DebugConfig 表转换为 SDK 对象。""" + if value is None or not isinstance(value, Mapping): + return value + allowed = {"client_mode", "replays_directory", "replay_id"} + unknown = sorted(set(value).difference(allowed)) + if unknown: + raise ValueError( + "Google debug_config 包含不支持的字段: " + ", ".join(unknown) + ) + try: + from google.genai.client import DebugConfig + except ImportError as exc: + raise RuntimeError("当前 google-genai SDK 不支持 debug_config。") from exc + try: + return DebugConfig(**dict(value)) + except Exception as exc: + raise ValueError("Google debug_config 配置无效。") from exc + + def _configured_vertex_mode(self) -> bool | None: + """读取显式或 SDK 支持的环境级 Vertex/Enterprise 开关。""" + options = getattr(self, "_options", {}) or {} + explicit_values = { + key: self._as_bool(options[key]) + for key in ("enterprise", "vertexai") + if key in options and options[key] is not None + } + if explicit_values: + if len(set(explicit_values.values())) > 1: + raise ValueError( + "Google options.enterprise 与 options.vertexai 的值冲突;" + "请只设置一个,或将两者设为相同值。" + ) + return next(iter(explicit_values.values())) + + settings = get_settings() + environment_values = self._configured_environment_vertex_flags(settings) + if len(set(environment_values.values())) > 1: + raise ValueError( + "GOOGLE_GENAI_USE_ENTERPRISE 与 GOOGLE_GENAI_USE_VERTEXAI 的值冲突;" + "请只保留一个,或将两者设为相同值。" + ) + # 未配置开关时保持 unknown,而不是武断地当作 Developer API。 + # 这样测试替身、旧 SDK 或由 SDK 自己决定 endpoint 的客户端仍能 + # 继续执行能力探测;明确设置 false 时才拒绝 Vertex-only 能力。 + return next(iter(environment_values.values())) if environment_values else None + + @classmethod + def _configured_environment_vertex_flags(cls, settings: Any) -> dict[str, bool]: + """读取进程环境或 ``.env`` 载入的 Vertex/Enterprise 开关。""" + + values: dict[str, bool] = {} + for env_name, field_name in ( + ("GOOGLE_GENAI_USE_ENTERPRISE", "google_genai_use_enterprise"), + ("GOOGLE_GENAI_USE_VERTEXAI", "google_genai_use_vertexai"), + ): + raw_value = os.getenv(env_name) + if raw_value is None: + raw_value = getattr(settings, field_name, None) + # MagicMock-style test doubles expose arbitrary attributes; + # those are not valid Settings values and must be ignored. + if not isinstance(raw_value, (bool, int, float, str)): + raw_value = None + if raw_value is not None: + values[env_name] = cls._as_bool(raw_value) + return values + + @staticmethod + def _client_vertex_mode(client: Any) -> bool | None: + """返回已构造客户端的真实模式;测试替身没有该属性时返回 None。""" + client_values = vars(client) if hasattr(client, "__dict__") else {} + api_client = client_values.get("_api_client") + api_values = vars(api_client) if hasattr(api_client, "__dict__") else {} + if api_client is not None and "vertexai" in api_values: + return bool(api_client.vertexai) + if "vertexai" in client_values: + return bool(client_values["vertexai"]) + return None + + def _effective_vertex_mode(self, client: Any) -> bool | None: + """优先读取 SDK 实际模式,测试替身或旧 SDK 缺失时回退到配置。""" + actual = self._client_vertex_mode(client) + return self._configured_vertex_mode() if actual is None else actual + + def _vertex_embed_content_only(self) -> bool: + """判断当前模型是否只能通过 Vertex 的单内容 embedContent 调用。""" + model = self._model_name.lower() + return ( + "gemini" in model + ) or "maas" in model + + @staticmethod + def _validate_token_count_fields(request: CompletionRequest) -> None: + """拒绝 token 统计不会接受或不会生效的生成参数。""" + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key in { + "max_tokens", "top_p", "top_k", "frequency_penalty", + "presence_penalty", "n", "logit_bias", "logprobs", + "top_logprobs", "modalities", "audio", "prediction", + "web_search_options", "seed", "stop", "response_format", + "tool_choice", "reasoning", "thinking", "store", "background", + "parallel_tool_calls", "max_tool_calls", "conversation", "session", + "context_management", "caching", "expire_at", "prompt_cache_options", + "safety_identifier", "moderation", "verbosity", "prompt_cache_key", + "prompt_cache_retention", "service_tier", "truncation", "stream_options", + "cache_control", "container", "inference_geo", "mcp_servers", + "output_config", "output_format", "speed", "betas", "diagnostics", + "fallback_credit_token", "fallbacks", "user", "personality", + } + and value is not None + } + reject_unsupported_kwargs("Google token count", unsupported) + + @staticmethod + def _validate_compute_token_fields(request: CompletionRequest) -> None: + """拒绝 computeTokens 不接受或不会生效的请求字段。""" + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key in { + "max_tokens", "top_p", "top_k", "frequency_penalty", + "presence_penalty", "n", "logit_bias", "logprobs", "top_logprobs", + "modalities", "audio", "prediction", "web_search_options", "seed", + "stop", "response_format", "tool_choice", "reasoning", "thinking", + "store", "background", "parallel_tool_calls", "max_tool_calls", + "conversation", "session", "context_management", "caching", "expire_at", + "prompt_cache_options", "safety_identifier", "moderation", "verbosity", + "prompt_cache_key", "prompt_cache_retention", "service_tier", "truncation", + "stream_options", "cache_control", "container", "inference_geo", "mcp_servers", + "output_config", "output_format", "speed", "betas", "diagnostics", + "fallback_credit_token", "fallbacks", "user", "personality", "tools", + } + and value is not None + } + reject_unsupported_kwargs("Google compute tokens", unsupported) + + @staticmethod + def _token_count_system_prompt(request: CompletionRequest) -> str | None: + """返回 token 统计请求中真正显式提供的 system prompt。""" + message_system = extract_system_prompt(request.messages) + if message_system: + return message_system + default_system_prompt = CompletionRequest.__dataclass_fields__["system_prompt"].default + return None if request.system_prompt == default_system_prompt else request.system_prompt + + def _embedding_options(self) -> dict[str, Any]: + supported = { + "task_type", "output_dimensionality", "title", "mime_type", + "auto_truncate", "document_ocr", "audio_track_extraction", + } + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _resource_config(config_type: Any, config: Any, values: dict[str, Any]) -> Any: + """将 Facade 的关键字配置转换为 google-genai 的 ``config=`` 参数。""" + GoogleProvider._validate_resource_config(config) + values = dict(values) + request_options = { + key: values.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + http_options = GoogleProvider._build_http_options(**request_options) + if http_options is not None: + if "http_options" in values: + raise ValueError("Google SDK 的 http_options 不能与请求级 HTTP 配置同时传入。") + values["http_options"] = http_options + if config is not None: + if values: + raise ValueError("Google SDK 的 config 不能与同级配置关键字同时传入。") + return config + if not values: + return None + return config_type(**values) + + @classmethod + def _resource_config_kwargs( + cls, config_type: Any, config: Any, values: dict[str, Any] + ) -> dict[str, Any]: + built = cls._resource_config(config_type, config, values) + return {"config": built} if built is not None else {} + + @staticmethod + def _validate_embedding_kwargs(kwargs: dict[str, Any]) -> None: + allowed = { + "task_type", "output_dimensionality", "title", "mime_type", + "auto_truncate", "document_ocr", "audio_track_extraction", + } + unsupported = sorted(key for key, value in kwargs.items() if value is not None and key not in allowed) + if unsupported: + raise ValueError(f"Google Embedding 不支持请求参数: {', '.join(unsupported)}") + + @staticmethod + def _validate_request(request: CompletionRequest) -> None: + supported = { + "prompt", "system_prompt", "messages", "tools", "stream", "temperature", + "max_tokens", "top_p", "top_k", "frequency_penalty", "presence_penalty", + "n", "seed", "modalities", "stop", "response_format", "tool_choice", + "extra_body", "extra_headers", "extra_query", "thinking", "service_tier", "timeout", + } + reject_unsupported_kwargs( + "Google SDK", + { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in supported + }, + ) + + @staticmethod + def _build_http_options( + *, + extra_headers: dict[str, str] | None = None, + extra_body: dict[str, Any] | None = None, + extra_query: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> Any | None: + """将统一请求的 HTTP 扩展转换为 google-genai 的 HttpOptions。""" + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Google", + ) + if extra_query is not None: + raise ValueError( + "Google SDK 不支持请求级 extra_query;请在 options.http_options 中配置。" + ) + extra_body = validate_secret_free_payload( + extra_body, + "Google", + "extra_body", + ) if extra_body is not None else None + values: dict[str, Any] = {} + if extra_headers is not None: + values["headers"] = dict(extra_headers) + if extra_body is not None: + values["extra_body"] = dict(extra_body) + if timeout is not None: + timeout = GoogleProvider._validate_timeout(timeout, "Google 请求 timeout") + values["timeout"] = max(1, int(timeout * 1000)) + return types.HttpOptions(**values) if values else None + + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + return await result if inspect.isawaitable(result) else result + + def _generation_options(self) -> dict[str, Any]: + supported = { + "temperature", "top_p", "top_k", "candidate_count", "max_output_tokens", + "stop_sequences", "presence_penalty", "frequency_penalty", "seed", + "response_mime_type", "response_schema", "response_json_schema", + "safety_settings", "cached_content", "response_modalities", "thinking_config", + "automatic_function_calling", "speech_config", "image_config", "labels", + "routing_config", "model_selection_config", "media_resolution", + "enable_enhanced_civic_answers", "service_tier", + "logprobs", "response_logprobs", "audio_timestamp", "audio_transcription_config", + "model_armor_config", "should_return_http_response", + } + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _response_format_values(response_format: Any) -> dict[str, Any]: + """将统一 response_format 转换为 Gemini GenerateContentConfig 字段。""" + if not isinstance(response_format, Mapping): + raise ValueError("Google response_format 必须是对象。") + format_type = response_format.get("type") + if format_type == "json_object": + return {"response_mime_type": "application/json"} + if format_type == "json_schema": + schema = response_format.get("json_schema", response_format) + if not isinstance(schema, Mapping): + raise ValueError("Google response_format.json_schema 必须是对象。") + payload = schema.get("schema", response_format.get("schema")) + if not isinstance(payload, Mapping): + raise ValueError( + "Google response_format.json_schema.schema 必须是 JSON Schema 对象。" + ) + return { + "response_mime_type": "application/json", + "response_json_schema": dict(payload), + } + raise ValueError( + "Google response_format 仅支持 json_object 或 json_schema。" + ) + + def _generation_http_options( + self, + *, + extra_body: dict[str, Any] | None, + extra_headers: dict[str, str] | None, + extra_query: dict[str, Any] | None, + timeout: float | None, + ) -> tuple[dict[str, Any] | None, dict[str, str] | None, dict[str, Any] | None, float | None, Any | None]: + """合并模型级与调用级 HTTP 扩展,拒绝同名字段静默覆盖。""" + options = getattr(self, "_options", {}) or {} + configured_body = options.get("extra_body") + if configured_body is not None and not isinstance(configured_body, Mapping): + raise ValueError("Google options.extra_body 必须是对象。") + configured_body = validate_secret_free_payload( + configured_body, + "Google", + "options.extra_body", + ) + if extra_body is not None and not isinstance(extra_body, Mapping): + raise ValueError("Google extra_body 必须是对象。") + configured_values = dict(configured_body or {}) + request_values = validate_secret_free_payload( + extra_body, + "Google", + "extra_body", + ) + overlap = sorted(set(configured_values).intersection(request_values)) + if overlap: + raise ValueError( + "Google extra_body 与模型 options 重复: " + ", ".join(overlap) + ) + merged_body = {**configured_values, **request_values} or None + effective_timeout = timeout + if effective_timeout is None: + effective_timeout = options.get("timeout") + configured_http_options = options.get("http_options") + request_body_configured = request_values if request_values else None + if configured_http_options is not None and any( + value is not None + for value in (extra_headers, request_body_configured, extra_query, timeout) + ): + raise ValueError( + "Google options.http_options 不能与请求级 extra_headers、extra_body、" + "extra_query 或 timeout 同时传入。" + ) + return ( + merged_body, + dict(extra_headers) if extra_headers is not None else None, + dict(extra_query) if extra_query is not None else None, + effective_timeout, + configured_http_options, + ) + + def _http_options_for_request( + self, + *, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> Any | None: + """构造模型级与请求级 HTTP 配置共用的 ``HttpOptions``。 + + Embedding、token-count 和 compute-token 接口不经过生成配置构造器, + 因此必须显式复用同一套模型级 ``extra_body``、``timeout`` 和 + ``http_options`` 语义,避免这些入口悄悄丢弃配置。 + """ + merged_body, request_headers, request_query, effective_timeout, configured = ( + self._generation_http_options( + extra_body=extra_body, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + ) + ) + if configured is not None: + return configured + return self._build_http_options( + extra_headers=request_headers, + extra_body=merged_body, + extra_query=request_query, + timeout=effective_timeout, + ) + + @staticmethod + def _convert_tools(tools: list[dict[str, Any]] | None) -> list[Any] | None: + if not tools: + return None + + converted_tools: list[Any] = [] + declarations = [] + for tool in tools: + if not isinstance(tool, dict): + converted_tools.append(tool) + continue + native_keys = { + "retrieval", "computer_use", "file_search", "google_search", "google_maps", + "code_execution", "enterprise_web_search", "google_search_retrieval", + "parallel_ai_search", "url_context", "mcp_servers", "exa_ai_search", + } + type_name = tool.get("type") + type_aliases = { + "google_search": "google_search", + "code_execution": "code_execution", + "url_context": "url_context", + "file_search": "file_search", + } + if type_name in type_aliases: + native_value = tool.get(type_name, {}) + if native_value is True or native_value is None: + native_value = {} + converted_tools.append(types.Tool(**{type_aliases[type_name]: native_value})) + continue + if native_keys.intersection(tool): + converted_tools.append(types.Tool(**tool)) + continue + function = tool.get("function", tool) + name = function.get("name") + if not name: + raise ValueError("Google 工具定义缺少 function.name。") + declarations.append( + types.FunctionDeclaration( + name=name, + description=function.get("description"), + parameters_json_schema=function.get( + "parameters", {"type": "object", "properties": {}} + ), + ) + ) + if declarations: + converted_tools.insert(0, types.Tool(function_declarations=declarations)) + return converted_tools + + @staticmethod + def _convert_tool_choice(tool_choice: Any) -> Any: + if tool_choice is None: + return None + if isinstance(tool_choice, dict) and "function_calling_config" in tool_choice: + return types.ToolConfig(**tool_choice) + if isinstance(tool_choice, dict): + if tool_choice.get("type") == "function": + function = tool_choice.get("function", {}) + if not isinstance(function, Mapping): + raise ValueError("Google function tool_choice 的 function 必须是对象。") + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Google function tool_choice 缺少 function.name。") + return types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=types.FunctionCallingConfigMode.ANY, + allowed_function_names=[name], + ) + ) + tool_choice = tool_choice.get("type", tool_choice.get("mode", "AUTO")) + if isinstance(tool_choice, str): + mode = {"auto": "AUTO", "required": "ANY", "any": "ANY", "none": "NONE"}.get( + tool_choice.lower(), tool_choice.upper() + ) + return types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=types.FunctionCallingConfigMode(mode) + ) + ) + return tool_choice + + def _build_generation_config( + self, + system_prompt: str | None, + tools: list[dict[str, Any]] | None, + temperature: float | None, + *, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + candidate_count: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: Sequence[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, + ) -> Any: + values: dict[str, Any] = self._generation_options() + configured_max_tokens = self._options.get("max_tokens") + configured_max_output_tokens = values.get("max_output_tokens") + if configured_max_tokens is not None and configured_max_output_tokens is not None: + raise ValueError( + "Google options.max_tokens 与 options.max_output_tokens 重复。" + ) + if configured_max_tokens is not None: + values["max_output_tokens"] = configured_max_tokens + configured_thinking = self._options.get("thinking") + configured_thinking_config = values.get("thinking_config") + if configured_thinking is not None and configured_thinking_config is not None: + raise ValueError( + "Google options.thinking 与 options.thinking_config 重复。" + ) + if configured_thinking is not None: + values["thinking_config"] = configured_thinking + configured_response_format = self._options.get("response_format") + if configured_response_format is not None: + if response_format is not None: + raise ValueError("Google response_format 与模型 options 重复。") + response_format = configured_response_format + values.update({ + "system_instruction": system_prompt, + "tools": self._convert_tools(tools), + }) + if temperature is not None or "temperature" not in values: + values["temperature"] = 0.7 if temperature is None else temperature + if max_tokens is not None: + values["max_output_tokens"] = max_tokens + if top_p is not None: + values["top_p"] = top_p + if top_k is not None: + values["top_k"] = top_k + if candidate_count is not None: + values["candidate_count"] = candidate_count + if frequency_penalty is not None: + values["frequency_penalty"] = frequency_penalty + if presence_penalty is not None: + values["presence_penalty"] = presence_penalty + if seed is not None: + values["seed"] = seed + if modalities is not None: + values["response_modalities"] = modalities + if stop is not None: + values["stop_sequences"] = [stop] if isinstance(stop, str) else stop + if response_format is not None: + values.update(self._response_format_values(response_format)) + if tool_choice is not None: + values["tool_config"] = self._convert_tool_choice(tool_choice) + if thinking is not None: + values["thinking_config"] = thinking + if service_tier is not None: + values["service_tier"] = service_tier + merged_body, request_headers, request_query, effective_timeout, configured_http_options = ( + self._generation_http_options( + extra_body=extra_body, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + ) + ) + if configured_http_options is not None: + values["http_options"] = configured_http_options + else: + http_options = self._build_http_options( + extra_headers=request_headers, + extra_body=merged_body, + extra_query=request_query, + timeout=effective_timeout, + ) + if http_options is not None: + values["http_options"] = http_options + return types.GenerateContentConfig(**{key: value for key, value in values.items() if value is not None}) + + @staticmethod + def _decode_data_uri(value: str, context: str) -> tuple[str, bytes]: + """严格解码 data URI,避免 ``b64decode`` 静默丢弃非法字符。""" + if "," not in value: + raise ValueError(f"Google {context} 的 data URI 缺少逗号分隔符。") + header, encoded = value.split(",", 1) + mime_type = header.split(";", 1)[0].removeprefix("data:") + if not mime_type: + raise ValueError(f"Google {context} 的 data URI 缺少 MIME 类型。") + try: + decoded = base64.b64decode(encoded, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError(f"Google {context} 的 data URI 不是有效 Base64。") from exc + return mime_type, decoded + + @staticmethod + def _contents( + messages: Any, + prompt: str | None, + system_prompt: str | None, + *, + vertex_mode: bool | None = None, + ) -> Any: + """将统一消息转换为 Gemini 内容,并按客户端模式校验媒体 URI。 + + Developer API 支持 Gemini Files API 的 ``file_id`` 映射;Vertex/Enterprise + 只接受 Cloud Storage URI 或可公开读取的 HTTPS URI。模式未知时保持 + Developer API 的历史兼容行为,实际 provider 调用会传入 SDK 检测结果。 + """ + normalized = normalize_messages(prompt, system_prompt, messages) + if messages is None and prompt is not None: + return prompt + contents: list[Any] = [] + tool_call_names: dict[str, str] = {} + pending_tool_parts: list[Any] = [] + + def append_content(role: str, parts: list[Any]) -> None: + """合并连续同角色消息,满足 Gemini user/model 交替约束。""" + if contents and field(contents[-1], "role") == role: + contents[-1].parts.extend(parts) + return + contents.append(types.Content(role=role, parts=parts)) + + def file_uri(value: Any, context: str) -> str: + """规范化媒体 URI,并拒绝当前端点不支持的凭证引用。""" + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"Google {context} 缺少有效 URI。") + normalized = value.strip() + if normalized.startswith("gs://"): + return normalized + google_files_prefix = "https://generativelanguage.googleapis.com/v1beta/files/" + file_name = normalized.removeprefix("files/") + if normalized.startswith("files/") or "://" not in normalized: + if ( + not file_name + or "/" in file_name + or not re.fullmatch(r"[A-Za-z0-9._~-]+", file_name) + ): + raise ValueError(f"Google {context} 的 file_id 无效。") + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式不支持 Gemini Files file_id;" + "请使用 gs:// URI 或可公开访问的 HTTPS URI。" + ) + return google_files_prefix + quote(file_name, safe="-._~") + try: + parsed = urlsplit(normalized) + except ValueError as exc: + raise ValueError(f"Google {context} URI 无效。") from exc + is_google_files_uri = ( + parsed.scheme.lower() == "https" + and parsed.hostname + and parsed.hostname.lower() == "generativelanguage.googleapis.com" + and re.search(r"/files/[A-Za-z0-9._~-]+$", parsed.path) + and not parsed.query + and not parsed.fragment + ) + if is_google_files_uri: + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式不支持 Gemini Files URI;" + "请使用 gs:// URI 或可公开访问的 HTTPS URI。" + ) + return normalized + if ( + vertex_mode is True + and parsed.scheme.lower() == "https" + and parsed.hostname + and parsed.username is None + and parsed.password is None + and not parsed.fragment + ): + # Vertex accepts externally hosted media only when the service + # can fetch it. We cannot prove public reachability locally, + # but reject malformed/userinfo URLs before handing them to + # the SDK; the service remains the authority on accessibility. + return normalized + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式仅支持 gs:// URI 或可公开访问的 HTTPS URI。" + ) + raise ValueError( + f"Google {context} 仅支持 data: URI、gs:// URI 或 Google Files URI,也可传 file_id。" + ) + + def media_mime_type(*sources: Any) -> str | None: + """读取媒体块显式提供的 MIME 类型,保持 SDK 的 MIME 透传。""" + for source in sources: + if not isinstance(source, Mapping): + continue + value = source.get( + "mime_type", + source.get("mimeType", source.get("media_type")), + ) + if value is None: + continue + if not isinstance(value, str) or not value.strip(): + raise ValueError("Google 媒体内容的 mime_type 必须是非空字符串。") + return value.strip() + return None + + def file_data(value: Any, context: str, mime_type: str | None = None) -> Any: + values: dict[str, Any] = {"file_uri": file_uri(value, context)} + if mime_type is not None: + values["mime_type"] = mime_type + return types.FileData(**values) + + def function_response_value(value: Any) -> Any: + if isinstance(value, str): + stripped = value.strip() + # Tool results are often plain text. Attempt JSON decoding for + # JSON-shaped values so malformed structured results still fail + # loudly, while preserving ordinary text as a valid output. + numeric_json = bool( + stripped + and stripped[0] in "-0123456789" + and re.fullmatch( + r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?", + stripped, + ) + ) + looks_like_json = ( + stripped.startswith(("{", "[", '"')) + or stripped in {"true", "false", "null"} + or numeric_json + ) + if not looks_like_json: + return {"output": value} + try: + parsed = json.loads(value) + except json.JSONDecodeError as exc: + raise ValueError("Google 工具结果的 content 不是有效 JSON。") from exc + return dict(parsed) if isinstance(parsed, Mapping) else {"output": parsed} + if isinstance(value, Mapping): + return dict(value) + return {"output": value} + + for message in normalized: + if message.get("role") == "system": + continue + message_role = message.get("role") + if message_role == "tool": + tool_call_id = message.get("tool_call_id") or message.get("id") + explicit_name = message.get("name") or message.get("tool_name") + name = explicit_name + if not name and isinstance(tool_call_id, str): + name = tool_call_names.get(tool_call_id) + if not name: + raise ValueError( + "Google 工具结果缺少可关联的 tool_call_id/name。" + ) + raw_content = message.get("content", message.get("response", {})) + response_parts = raw_content if isinstance(raw_content, list) else [raw_content] + parts = [] + for response_part in response_parts: + if isinstance(response_part, dict) and response_part.get("type") in { + "tool_result", "function_result", "function_response" + }: + response = response_part.get( + "response", response_part.get("content", {}) + ) + part_name = response_part.get("name") or name + part_id = response_part.get("tool_call_id") or tool_call_id + else: + response = response_part + part_name = name + part_id = tool_call_id + response_value = function_response_value(response) + if part_id is not None: + parts.append( + types.Part( + function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=response_value, + ) + ) + ) + else: + parts.append( + types.Part( + function_response=types.FunctionResponse( + name=part_name, + response=response_value, + ) + ) + ) + # Gemini represents parallel function results as one user turn. + # Keep consecutive standard ``role=tool`` messages together so + # the model receives a single FunctionResponse content block. + pending_tool_parts.extend(parts) + continue + + if pending_tool_parts: + append_content("user", pending_tool_parts) + pending_tool_parts = [] + + role = "model" if message_role == "assistant" else "user" + raw_content = message.get("content", "") + if raw_content is None: + parts = [] + elif isinstance(raw_content, str): + parts = [types.Part(text=raw_content)] + elif isinstance(raw_content, list): + parts = [] + for part in raw_content: + if isinstance(part, str): + parts.append(types.Part(text=part)) + continue + if not isinstance(part, dict): + parts.append(part) + continue + part_type = part.get("type") + if part_type in {"text", "input_text"}: + parts.append(types.Part(text=part.get("text", ""))) + elif part_type in {"image_url", "input_image"}: + image_value = part.get("image_url") + image_file_id = part.get("file_id") + if isinstance(image_value, Mapping): + image_url = image_value.get( + "url", + image_value.get("image_url", image_value.get("file_uri")), + ) + image_file_id = image_file_id or image_value.get("file_id") + else: + image_url = image_value or part.get("url") + if image_url is None and image_file_id is not None: + image_url = image_file_id + if not isinstance(image_url, str) or not image_url.strip(): + raise ValueError("Google 图片内容缺少有效 URI。") + image_url = image_url.strip() + if isinstance(image_url, str) and image_url.startswith("data:"): + mime_type, data = GoogleProvider._decode_data_uri(image_url, "图片") + parts.append( + types.Part( + inline_data=types.Blob(mime_type=mime_type, data=data) + ) + ) + else: + parts.append( + types.Part( + file_data=file_data( + image_url, + "图片", + media_mime_type(part, image_value), + ) + ) + ) + elif part_type in {"audio_url", "input_audio", "video_url", "input_video", "file"}: + media_value = part.get( + "audio_url", + part.get("video_url", part.get("url", part.get("file_uri"))), + ) + media_file_id = part.get("file_id") + if part_type == "file" and media_value is None: + media_value = part.get("file") + if isinstance(media_value, Mapping): + media = media_value.get( + "url", + media_value.get( + "file_uri", + media_value.get("audio_url", media_value.get("video_url")), + ), + ) + media_file_id = media_file_id or media_value.get("file_id") + else: + media = media_value + if media is None and media_file_id is not None: + media = media_file_id + if not isinstance(media, str) or not media.strip(): + raise ValueError("Google 媒体内容缺少有效 URI。") + media = media.strip() + if isinstance(media, str) and media.startswith("data:"): + mime_type, data = GoogleProvider._decode_data_uri(media, "媒体") + parts.append( + types.Part( + inline_data=types.Blob( + mime_type=mime_type, + data=data, + ) + ) + ) + else: + parts.append( + types.Part( + file_data=file_data( + media, + "媒体", + media_mime_type(part, media_value), + ) + ) + ) + elif part_type in {"tool_result", "function_result"}: + part_id = part.get("tool_call_id") or part.get("id") + part_name = part.get("name") or part.get("tool_name") + if not part_name and isinstance(part_id, str): + part_name = tool_call_names.get(part_id) + if not isinstance(part_name, str) or not part_name.strip(): + raise ValueError( + "Google 工具结果缺少可关联的 tool_call_id/name。" + ) + parts.append( + types.Part( + function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=function_response_value( + part.get("response", part.get("content", {})) + ), + ) + ) + ) + elif part_type == "function_call": + call_id = part.get("id") or part.get("call_id") + function_name = part.get("name") + if not isinstance(function_name, str) or not function_name.strip(): + raise ValueError("Google function_call 缺少 name。") + if call_id and function_name: + tool_call_names[call_id] = function_name + arguments = part.get("arguments", part.get("args", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + "Google function_call 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError("Google function_call 的 arguments 必须是 JSON 对象。") + parts.append(types.Part(function_call=types.FunctionCall( + id=call_id, name=function_name, args=dict(arguments) + ))) + elif part_type == "function_response": + part_id = part.get("tool_call_id") or part.get("id") + part_name = part.get("name") or part.get("tool_name") + if not part_name and isinstance(part_id, str): + part_name = tool_call_names.get(part_id) + if not isinstance(part_name, str) or not part_name.strip(): + raise ValueError( + "Google function_response 缺少可关联的 tool_call_id/name。" + ) + parts.append(types.Part(function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=function_response_value(part.get("response", {})), + ))) + else: + raise ValueError( + f"Google 不支持的消息内容块类型: {part_type!r}。" + ) + else: + parts = [types.Part(text=str(raw_content))] + for index, call in enumerate(message.get("tool_calls", []) or []): + if not isinstance(call, Mapping): + raise ValueError(f"Google assistant tool_calls[{index}] 必须是对象。") + function = call.get("function", call) + if not isinstance(function, Mapping): + raise ValueError( + f"Google assistant tool_calls[{index}].function 必须是对象。" + ) + call_id = call.get("id") or call.get("call_id") + function_name = function.get("name", "") + if not isinstance(function_name, str) or not function_name.strip(): + raise ValueError( + f"Google assistant tool_calls[{index}] 缺少 function.name。" + ) + if call_id and function_name: + tool_call_names[call_id] = function_name + arguments = function.get("arguments", function.get("args", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + f"Google assistant tool_calls[{index}] 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError("Google assistant tool call 的 arguments 必须是 JSON 对象。") + parts.append( + types.Part( + function_call=types.FunctionCall( + id=call_id, + name=function.get("name", ""), args=dict(arguments) + ) + ) + ) + append_content(role, parts) + if pending_tool_parts: + append_content("user", pending_tool_parts) + return contents + + @staticmethod + def _response_text(response: Any) -> str: + """读取文本而不触发 Google SDK 纯工具响应的 ValueError。""" + candidates = field(response, "candidates", []) or [] + if candidates: + chunks: list[str] = [] + saw_parts = False + for candidate in candidates: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + saw_parts = True + if field(part, "thought", False): + continue + text = field(part, "text", "") + if isinstance(text, str): + chunks.append(text) + if saw_parts: + return "".join(chunks) + try: + value = field(response, "text", "") + except Exception: # noqa: BLE001 - SDK response accessors are dynamic + value = "" + if isinstance(value, str): + return value + chunks = [] + for candidate in candidates: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + if field(part, "thought", False): + continue + text = field(part, "text", "") + if isinstance(text, str): + chunks.append(text) + return "".join(chunks) + + _REFUSAL_FINISH_REASONS = frozenset( + { + "SAFETY", + "BLOCKLIST", + "PROHIBITED_CONTENT", + "SPII", + "RECITATION", + "IMAGE_SAFETY", + "MODEL_ARMOR", + } ) + + @classmethod + def _candidate_refusal(cls, candidate: Any) -> str | None: + """读取 Gemini 内容块或安全拦截元数据中的拒答原因。""" + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + value = field(part, "refusal") + if not value and field(part, "type") == "refusal": + value = field(part, "text") or field(part, "reason") + if isinstance(value, str) and value: + return value + + for key in ("finish_message", "block_reason_message"): + value = field(candidate, key) + if isinstance(value, str) and value: + return value + + finish_reason = field(candidate, "finish_reason") + normalized_reason = cls._normalize_finish_reason(finish_reason) or "" + if normalized_reason in cls._REFUSAL_FINISH_REASONS: + return f"Google 内容被安全策略拦截: {normalized_reason}" + return None + + @staticmethod + def _normalize_finish_reason(value: Any) -> str | None: + """将 google-genai 枚举或原始值统一为协议中的字符串。""" + if value is None: + return None + name = getattr(value, "name", None) + if isinstance(name, str) and name.strip(): + return name.strip().upper() + raw = getattr(value, "value", value) + text = str(raw).strip() + if not text: + return None + if "." in text: + text = text.rsplit(".", 1)[-1] + return text.upper() + + @classmethod + def _extract_refusal(cls, response: Any) -> str | None: + """提取 Gemini 响应中的显式拒答或安全拦截信息。""" + for candidate in field(response, "candidates", []) or []: + refusal = cls._candidate_refusal(candidate) + if refusal: + return refusal + feedback = field(response, "prompt_feedback") + block_reason = field(feedback, "block_reason") + if isinstance(block_reason, Enum): + block_reason_text = block_reason.name or str(block_reason) + elif isinstance(block_reason, str): + block_reason_text = block_reason.strip() + else: + block_reason_text = "" + if block_reason_text: + message = field(feedback, "block_reason_message") + if not isinstance(message, str) or not message.strip(): + message = field(feedback, "message") + return str(message or f"Google 提示被安全策略拦截: {block_reason_text}") + return None + + @staticmethod + def _partial_arg_value(partial: Any) -> Any: + """读取 Vertex ``PartialArg`` 的标量值。""" + for name in ("bool_value", "number_value", "string_value"): + value = field(partial, name) + if value is not None: + return value + if field(partial, "null_value") is not None: + return None + raise ValueError("Google FunctionCall.partial_args 缺少有效值。") + + @staticmethod + def _partial_arg_tokens(json_path: str) -> list[str | int]: + """解析 Vertex PartialArg 使用的常见 JSONPath 子集。""" + if not isinstance(json_path, str) or not json_path.startswith("$"): + raise ValueError(f"Google FunctionCall.partial_args 的 json_path 无效: {json_path!r}") + if json_path in {"", "$"}: + return [] + rest = json_path[1:] + tokens: list[str | int] = [] + while rest: + dotted = re.match(r"^\.([A-Za-z_][A-Za-z0-9_]*)", rest) + if dotted: + tokens.append(dotted.group(1)) + rest = rest[dotted.end():] + continue + indexed = re.match(r"^\[(\d+)\]", rest) + if indexed: + tokens.append(int(indexed.group(1))) + rest = rest[indexed.end():] + continue + quoted = re.match(r"^\[['\"]([^'\"]+)['\"]\]", rest) + if quoted: + tokens.append(quoted.group(1)) + rest = rest[quoted.end():] + continue + raise ValueError( + f"Google FunctionCall.partial_args 不支持的 json_path: {json_path!r}" + ) + return tokens + + @classmethod + def _partial_args_to_mapping(cls, partial_args: Any) -> dict[str, Any]: + """将 Vertex 的 PartialArg 列表还原为普通 JSON 对象。""" + if isinstance(partial_args, Mapping): + return dict(partial_args) + if not isinstance(partial_args, (list, tuple)): + raise ValueError("Google FunctionCall.partial_args 必须是列表。") + result: dict[str, Any] = {} + for partial in partial_args: + json_path = field(partial, "json_path") + tokens = cls._partial_arg_tokens(json_path or "$") + value = cls._partial_arg_value(partial) + if not tokens: + if not isinstance(value, Mapping): + raise ValueError( + "Google FunctionCall.partial_args 的根值必须是 JSON 对象。" + ) + result.update(dict(value)) + continue + current: Any = result + for position, token in enumerate(tokens): + last = position == len(tokens) - 1 + if isinstance(token, int): + if not isinstance(current, list) or token < 0: + raise ValueError( + f"Google FunctionCall.partial_args 的 json_path 无法写入: {json_path!r}" + ) + while len(current) <= token: + current.append(None) + if last: + current[token] = value + elif current[token] is None: + current[token] = [] if isinstance(tokens[position + 1], int) else {} + current = current[token] + else: + if not isinstance(current, dict): + raise ValueError( + f"Google FunctionCall.partial_args 的 json_path 无法写入: {json_path!r}" + ) + if last: + current[token] = value + else: + current.setdefault( + token, + [] if isinstance(tokens[position + 1], int) else {}, + ) + current = current[token] + return result + + @classmethod + def _function_call_arguments(cls, function_call: Any) -> Any: + """统一读取完整 args 与 Vertex 流式 partial_args。""" + arguments = field(function_call, "args") + if arguments is not None: + return arguments + partial_args = field(function_call, "partial_args") + if partial_args is not None: + return cls._partial_args_to_mapping(partial_args) + return {} + + @staticmethod + def _function_call_index(function_call: Any) -> Any: + """读取仅由兼容字典载荷提供的工具调用 index。 + + ``google-genai`` 的 ``types.FunctionCall`` 没有 index 字段;并行 + 调用的 index 不能用 Candidate.index 代替,因为后者标识候选答案, + 不是候选内部的工具调用。保留字典读取仅用于中转网关的扩展载荷。 + """ + return function_call.get("index") if isinstance(function_call, Mapping) else None + + @classmethod + def _merge_tool_arguments(cls, existing: Any, incoming: Any) -> Any: + """递归合并工具参数分片,保留不同分片中的嵌套字段。""" + if isinstance(existing, Mapping) and isinstance(incoming, Mapping): + merged_mapping: dict[Any, Any] = dict(existing) + for key, value in incoming.items(): + merged_mapping[key] = ( + cls._merge_tool_arguments(merged_mapping[key], value) + if key in merged_mapping + else value + ) + return merged_mapping + if isinstance(existing, list) and isinstance(incoming, list): + merged_list: list[Any] = list(existing) + for index, value in enumerate(incoming): + if index >= len(merged_list): + merged_list.extend([None] * (index + 1 - len(merged_list))) + if value is not None: + merged_list[index] = ( + cls._merge_tool_arguments(merged_list[index], value) + if merged_list[index] is not None + else value + ) + return merged_list + return incoming + + @classmethod + def _merge_tool_call( + cls, + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + fallback_index: Any = None, + *, + merge_arguments: bool = True, + ) -> tuple[Any, dict[str, Any]]: + """累积 Gemini FunctionCall 分片,兼容字典参数与字符串参数。 + + ``tool_call_completed`` 有时会紧跟同一个 chunk 的 delta,并重复携带 + 完整 arguments。完成事件只需要复用已累积的元数据;调用方可将 + ``merge_arguments`` 设为 ``False``,避免字符串参数被追加两次。 + """ + call_id = tool_call.get("id") or tool_call.get("call_id") + index = tool_call.get("index", fallback_index) + name = tool_call.get("name") + + def has_identity(candidate: Mapping[str, Any]) -> bool: + return bool(candidate.get("id") or candidate.get("call_id")) + + def candidate_matches_id(candidate: Mapping[str, Any]) -> bool: + candidate_id = candidate.get("id") or candidate.get("call_id") + return bool(call_id and candidate_id == call_id) + + def name_matches(candidate: Mapping[str, Any]) -> bool: + candidate_name = candidate.get("name") + return name is None or candidate_name in {None, name} + + def move_candidate(candidate_key: Any, target_key: Any) -> Any: + if candidate_key == target_key: + return target_key + if target_key in accumulator: + raise RuntimeError( + "Google Gemini 工具调用分片的 id/index 指向多个调用。" + ) + accumulator[target_key] = accumulator.pop(candidate_key) + return target_key + + # google-genai's FunctionCall does not expose an index in the current + # SDK. Never synthesize one from a part position: that position resets + # in every response chunk and would merge independent parallel calls. + # An explicit index is the most stable stream identity. If a later + # fragment supplies an id, attach it to the existing indexed record + # instead of moving the accumulated arguments to a new id key. + if index is not None: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate.get("index") == index and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 index 指向多个调用。" + ) + if candidates: + key = candidates[0] + else: + # A provider may emit the id first and the index only on a + # later fragment. Match that id across the whole accumulator + # before falling back to a name-only, index-less record. + id_candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate_matches_id(candidate) + ] + if len(id_candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + if id_candidates: + key = move_candidate(id_candidates[0], ("index", index)) + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate.get("index") is None and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if len(candidates) == 1: + key = move_candidate(candidates[0], ("index", index)) + else: + key = ("index", index) + elif call_id: + exact_key = ("id", call_id) + id_candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate_matches_id(candidate) + ] + if len(id_candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + if id_candidates: + # Keep an existing explicit index as the accumulator key. This + # handles streams where the first fragment has both id/index + # and later fragments repeat only the id. + key = id_candidates[0] + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if not has_identity(candidate) and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if len(candidates) == 1: + # Preserve an existing explicit index. A position-only + # record has no stronger identity, so promote it to id. + candidate_key = candidates[0] + candidate = accumulator[candidate_key] + key = ( + candidate_key + if candidate.get("index") is not None + else move_candidate(candidate_key, exact_key) + ) + else: + key = exact_key + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if candidates: + candidate = accumulator[candidates[0]] + if ( + has_identity(candidate) + or tool_call.get("will_continue") is True + or candidate.get("will_continue") is True + ): + key = candidates[0] + else: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法确认是否为同一调用。" + ) + else: + position = len(accumulator) + key = ("position", position) + while key in accumulator: + position += 1 + key = ("position", position) + merged = accumulator.setdefault( + key, + {"type": "function", "arguments": {}}, + ) + existing_id = merged.get("id") or merged.get("call_id") + if call_id and existing_id and existing_id != call_id: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + existing_index = merged.get("index") + if index is not None and existing_index is not None and existing_index != index: + raise RuntimeError( + "Google Gemini 工具调用分片的 index 指向多个调用。" + ) + existing_name = merged.get("name") + if name and existing_name and existing_name != name: + raise RuntimeError( + "Google Gemini 工具调用分片的 name 指向多个调用。" + ) + for name in ("id", "call_id", "type", "name", "index", "will_continue"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + if merge_arguments: + arguments = tool_call.get("arguments") + if isinstance(arguments, Mapping): + existing = merged.get("arguments") + if not isinstance(existing, Mapping): + existing = {} + merged["arguments"] = cls._merge_tool_arguments(existing, arguments) + elif isinstance(arguments, str) and arguments: + existing = merged.get("arguments") + merged["arguments"] = (existing if isinstance(existing, str) else "") + arguments + elif arguments is not None: + merged["arguments"] = arguments + return key, merged + + @staticmethod + def _complete_stream_tool_calls( + accumulator: dict[Any, dict[str, Any]], + completed: set[Any], + response_id: str | None, + raw: Any, + ) -> list[StreamEvent]: + events: list[StreamEvent] = [] + for key, tool_call in accumulator.items(): + if key in completed: + continue + validate_complete_tool_call(tool_call, "Google Gemini") + completed.add(key) + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=raw, + ) + ) + return events + + @staticmethod + def _extract_result(response: Any) -> CompletionResult: + usage = field(response, "usage_metadata") or field(response, "usage") + usage_dict = normalize_usage(usage) + calls = [] + reasoning: list[str] = [] + for candidate in field(response, "candidates", []) or []: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + function_call = field(part, "function_call") + if function_call: + calls.append( + { + "id": field(function_call, "id"), + "type": "function", + "name": field(function_call, "name"), + "arguments": GoogleProvider._function_call_arguments(function_call), + } + ) + if field(part, "thought") and isinstance(field(part, "text"), str): + reasoning.append(field(part, "text")) + return CompletionResult( + text=GoogleProvider._response_text(response), + tool_calls=calls, + usage=usage_dict, + finish_reason=GoogleProvider._normalize_finish_reason( + field((field(response, "candidates", []) or [None])[0], "finish_reason") + ), + response_id=field(response, "response_id"), + refusal=GoogleProvider._extract_refusal(response), + reasoning="".join(reasoning), + raw=response, + ) + + @classmethod + def _stream_events_from_chunk(cls, chunk: Any) -> list[StreamEvent]: + events: list[StreamEvent] = [] + response_id = field(chunk, "response_id") + for candidate in field(chunk, "candidates", []) or []: + content = field(candidate, "content") + parts = field(content, "parts", []) or [] + refusal_emitted = False + for part in parts: + function_call = field(part, "function_call") + if function_call: + tool_call = { + "id": field(function_call, "id"), + "index": cls._function_call_index(function_call), + "type": "function", + "name": field(function_call, "name"), + "arguments": cls._function_call_arguments(function_call), + } + events.append( + StreamEvent( + type="tool_call_delta", + tool_call=tool_call, + response_id=response_id, + raw=chunk, + ) + ) + # Without id/index there is no safe way to distinguish an + # immediate completion event from a duplicate delta. The + # stream accumulator emits the completed call at finish, + # where the same ambiguity is checked explicitly. + if ( + field(function_call, "will_continue") is False + and ( + field(function_call, "id") + or field(function_call, "call_id") + or cls._function_call_index(function_call) is not None + ) + ): + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=tool_call, + response_id=response_id, + raw=chunk, + ) + ) + + part_type = field(part, "type") + part_refusal = field(part, "refusal") + if not part_refusal and part_type == "refusal": + part_refusal = field(part, "text") or field(part, "reason") + if isinstance(part_refusal, str) and part_refusal: + events.append( + StreamEvent( + type="refusal_delta", + refusal=part_refusal, + response_id=response_id, + raw=chunk, + ) + ) + refusal_emitted = True + continue + + text = field(part, "text") + if isinstance(text, str) and text: + if field(part, "thought"): + events.append( + StreamEvent( + type="reasoning_delta", + reasoning=text, + response_id=response_id, + raw=chunk, + ) + ) + else: + events.append( + StreamEvent( + type="text_delta", + text=text, + response_id=response_id, + raw=chunk, + ) + ) + + candidate_refusal = cls._candidate_refusal(candidate) + if candidate_refusal and not refusal_emitted: + events.append( + StreamEvent( + type="refusal_delta", + refusal=candidate_refusal, + response_id=response_id, + raw=chunk, + ) + ) + finish_reason = field(candidate, "finish_reason") + if finish_reason: + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(finish_reason), + response_id=response_id, + raw=chunk, + ) + ) + + # Safety blocks can arrive as ``prompt_feedback`` with no candidates. + # Treat that metadata as an explicit refusal and terminal event instead + # of allowing an empty stream to look like a successful response. + feedback = field(chunk, "prompt_feedback") + block_reason = field(feedback, "block_reason") + if block_reason: + message = field(feedback, "block_reason_message") or field(feedback, "message") + refusal = str(message or f"Google 提示被安全策略拦截: {block_reason}") + if not any(event.type == "refusal_delta" for event in events): + events.append( + StreamEvent( + type="refusal_delta", + refusal=refusal, + response_id=response_id, + raw=chunk, + ) + ) + if not any(event.type == "finish" for event in events): + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(block_reason), + response_id=response_id, + raw=chunk, + ) + ) + top_level_finish_reason = field(chunk, "finish_reason") + if ( + top_level_finish_reason is not None + and not any(event.type == "finish" for event in events) + ): + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(top_level_finish_reason), + response_id=response_id, + raw=chunk, + ) + ) + usage = normalize_usage(field(chunk, "usage_metadata") or field(chunk, "usage")) + if usage: + events.append(StreamEvent(type="usage", usage=usage, response_id=response_id, raw=chunk)) + return events + + @staticmethod + def _stream_chunk_has_terminal_event(chunk: Any) -> bool: + """判断 Gemini 流块是否明确表示本次生成已经终止。""" + if field(chunk, "finish_reason", None) is not None: + return True + feedback = field(chunk, "prompt_feedback") + if field(feedback, "block_reason", None) is not None: + return True + return any( + field(candidate, "finish_reason", None) is not None + for candidate in field(chunk, "candidates", []) or [] + ) + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Google complete") + self._validate_request(request) + system_prompt = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + system_prompt, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + response = retry_sync_call(lambda: client.models.generate_content( + model=self._model_name, + contents=self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ), + config=config, + )) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 google-genai 异步模型接口聚合完整结果。""" + request = coerce_completion_request(request, kwargs, "Google acomplete") + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + response = await retry_async_call(lambda: client.aio.models.generate_content( + model=self._model_name, + contents=self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ), + config=config, + )) + return self._extract_result(response) + + def count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Google count_tokens") + self._validate_token_count_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + config_values: dict[str, Any] = {} + # ``CompletionRequest`` keeps the legacy chat default system prompt for + # normal generation. It must not silently become an explicit + # ``system_instruction`` for a plain Developer API token-count call, + # where that field is unsupported. A caller-provided message-level + # system prompt remains explicit and is still validated below. + configured_system = self._token_count_system_prompt(request) + effective_system = configured_system + if vertexai is False and effective_system: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 system_prompt;" + "请设置 system_prompt=None,或改用 Vertex/Enterprise 模式。" + ) + if vertexai is False and request.tools: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 tools;请改用 Vertex/Enterprise 模式。" + ) + if vertexai is not False and effective_system: + config_values["system_instruction"] = effective_system + if vertexai is not False and request.tools: + config_values["tools"] = self._convert_tools(request.tools) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + if http_options is not None: + config_values["http_options"] = http_options + contents = self._contents( + request.messages, + request.prompt, + None if vertexai is False else configured_system, + vertex_mode=vertexai, + ) + response = retry_sync_call(lambda: client.models.count_tokens( + model=self._model_name, + contents=contents, + config=types.CountTokensConfig(**config_values) if config_values else None, + )) + value = field(response, "total_tokens") + if value is None: + raise RuntimeError("Google token count 响应缺少 total_tokens。") + return int(value) + + def compute_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Google compute_tokens") + self._validate_compute_token_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is False: + raise ValueError( + "Google compute_tokens 仅支持 Vertex/Enterprise 模式;" + "Gemini Developer API 不支持该能力。" + ) + effective_system = self._token_count_system_prompt(request) + if vertexai is True and effective_system: + raise ValueError( + "Google compute_tokens 不支持 system_prompt;请在 contents 中显式传入要统计的文本。" + ) + if vertexai is True and request.tools: + raise ValueError( + "Google compute_tokens 不支持 tools;请在 contents 中显式传入要统计的内容。" + ) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + config = types.ComputeTokensConfig(http_options=http_options) if http_options is not None else None + contents = self._contents( + request.messages, + request.prompt, + None, + vertex_mode=vertexai, + ) + return retry_sync_call(lambda: client.models.compute_tokens( + model=self._model_name, + contents=contents, + config=config, + )) + + def upload_file(self, file: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.upload( + file=file, + **self._resource_config_kwargs(types.UploadFileConfig, config, kwargs), + ) + + @staticmethod + def _validate_register_files_auth(auth: Any) -> Credentials: + """限制文件注册凭证为 google-auth 的官方 Credentials 对象。""" + if not isinstance(auth, Credentials): + raise ValueError( + "Google register_files auth 必须是 google.auth.credentials.Credentials 对象。" + ) + return auth + + def register_files(self, auth: Any, uris: list[str], config: Any = None, **kwargs: Any) -> Any: + auth = self._validate_register_files_auth(auth) + return self._get_client().files.register_files( + auth=auth, + uris=uris, + **self._resource_config_kwargs(types.RegisterFilesConfig, config, kwargs), + ) + + def get_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.get( + name=name, + **self._resource_config_kwargs(types.GetFileConfig, config, kwargs), + ) + + def list_files(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.list( + **self._resource_config_kwargs(types.ListFilesConfig, config, kwargs), + ) + + def download_file(self, file: Any, config: Any = None, **kwargs: Any) -> bytes: + return self._get_client().files.download( + file=file, + **self._resource_config_kwargs(types.DownloadFileConfig, config, kwargs), + ) + + def delete_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.delete( + name=name, + **self._resource_config_kwargs(types.DeleteFileConfig, config, kwargs), + ) + + def list_file_search_documents(self, parent: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.list( + parent=parent, + **self._resource_config_kwargs(types.ListDocumentsConfig, config, kwargs), + ) + + def get_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.get( + name=name, + **self._resource_config_kwargs(types.GetDocumentConfig, config, kwargs), + ) + + def delete_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.delete( + name=name, + **self._resource_config_kwargs(types.DeleteDocumentConfig, config, kwargs), + ) + + async def async_upload_file(self, file: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.upload( + file=file, + **self._resource_config_kwargs(types.UploadFileConfig, config, kwargs), + )) + + async def async_register_files(self, auth: Any, uris: list[str], config: Any = None, **kwargs: Any) -> Any: + auth = self._validate_register_files_auth(auth) + return await self._resolve_async_result( + self._get_client().aio.files.register_files( + auth=auth, + uris=uris, + **self._resource_config_kwargs(types.RegisterFilesConfig, config, kwargs), + ) + ) + + async def async_get_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.get( + name=name, + **self._resource_config_kwargs(types.GetFileConfig, config, kwargs), + )) + + async def async_list_files(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.list( + **self._resource_config_kwargs(types.ListFilesConfig, config, kwargs), + )) + + async def async_download_file(self, file: Any, config: Any = None, **kwargs: Any) -> bytes: + return await self._resolve_async_result(self._get_client().aio.files.download( + file=file, + **self._resource_config_kwargs(types.DownloadFileConfig, config, kwargs), + )) + + async def async_delete_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.delete( + name=name, + **self._resource_config_kwargs(types.DeleteFileConfig, config, kwargs), + )) + + async def async_list_file_search_documents(self, parent: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.list( + parent=parent, + **self._resource_config_kwargs(types.ListDocumentsConfig, config, kwargs), + ) + ) + + async def async_get_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.get( + name=name, + **self._resource_config_kwargs(types.GetDocumentConfig, config, kwargs), + ) + ) + + async def async_delete_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.delete( + name=name, + **self._resource_config_kwargs(types.DeleteDocumentConfig, config, kwargs), + ) + ) + + async def async_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Google async_count_tokens") + self._validate_token_count_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + config_values: dict[str, Any] = {} + configured_system = self._token_count_system_prompt(request) + effective_system = configured_system + if vertexai is False and effective_system: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 system_prompt;" + "请设置 system_prompt=None,或改用 Vertex/Enterprise 模式。" + ) + if vertexai is False and request.tools: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 tools;请改用 Vertex/Enterprise 模式。" + ) + if vertexai is not False and effective_system: + config_values["system_instruction"] = effective_system + if vertexai is not False and request.tools: + config_values["tools"] = self._convert_tools(request.tools) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + if http_options is not None: + config_values["http_options"] = http_options + contents = self._contents( + request.messages, + request.prompt, + None if vertexai is False else configured_system, + vertex_mode=vertexai, + ) + response = await retry_async_call(lambda: client.aio.models.count_tokens( + model=self._model_name, + contents=contents, + config=types.CountTokensConfig(**config_values) if config_values else None, + )) + value = field(response, "total_tokens") + if value is None: + raise RuntimeError("Google token count 响应缺少 total_tokens。") + return int(value) + + async def async_compute_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Google async_compute_tokens") + self._validate_compute_token_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is False: + raise ValueError( + "Google compute_tokens 仅支持 Vertex/Enterprise 模式;" + "Gemini Developer API 不支持该能力。" + ) + effective_system = self._token_count_system_prompt(request) + if vertexai is True and effective_system: + raise ValueError( + "Google compute_tokens 不支持 system_prompt;请在 contents 中显式传入要统计的文本。" + ) + if vertexai is True and request.tools: + raise ValueError( + "Google compute_tokens 不支持 tools;请在 contents 中显式传入要统计的内容。" + ) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + config = types.ComputeTokensConfig(http_options=http_options) if http_options is not None else None + contents = self._contents( + request.messages, + request.prompt, + None, + vertex_mode=vertexai, + ) + return await retry_async_call(lambda: client.aio.models.compute_tokens( + model=self._model_name, + contents=contents, + config=config, + )) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Google stream_events") + request = request.copy_with(stream=True) + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + contents = self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ) + if request.stream: + response = retry_sync_stream( + lambda: client.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in response: + for event in self._stream_events_from_chunk(chunk): + if event.type == "finish": + terminal_seen = True + yield from self._complete_stream_tool_calls( + tool_calls, + completed_tool_calls, + event.response_id, + event.raw, + ) + yield event + continue + if event.type == "tool_call_delta" and event.tool_call: + key, merged = self._merge_tool_call(tool_calls, event.tool_call) + event.tool_call = dict(merged) + yield event + continue + if event.type == "tool_call_completed" and event.tool_call: + previous_entries = tuple(tool_calls.values()) + key, merged = self._merge_tool_call( + tool_calls, + event.tool_call, + merge_arguments=False, + ) + if not any(merged is entry for entry in previous_entries): + # No matching delta was accumulated. Keep the + # normal merge path so a completion-only event + # still carries its arguments exactly once. + _, merged = self._merge_tool_call(tool_calls, event.tool_call) + if key in completed_tool_calls: + continue + validate_complete_tool_call(merged, "Google Gemini") + completed_tool_calls.add(key) + event.tool_call = dict(merged) + yield event + continue + yield event + if not terminal_seen: + raise RuntimeError("Google Gemini 流在终止事件之前结束。") + return + result = self.complete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Google astream_events") + request = request.copy_with(stream=True) + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + contents = self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ) + if request.stream: + response = retry_async_stream( + lambda: client.aio.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in iterate_async(response): + for event in self._stream_events_from_chunk(chunk): + if event.type == "finish": + terminal_seen = True + for completed_event in self._complete_stream_tool_calls( + tool_calls, + completed_tool_calls, + event.response_id, + event.raw, + ): + yield completed_event + yield event + continue + if event.type == "tool_call_delta" and event.tool_call: + key, merged = self._merge_tool_call(tool_calls, event.tool_call) + event.tool_call = dict(merged) + yield event + continue + if event.type == "tool_call_completed" and event.tool_call: + previous_entries = tuple(tool_calls.values()) + key, merged = self._merge_tool_call( + tool_calls, + event.tool_call, + merge_arguments=False, + ) + if not any(merged is entry for entry in previous_entries): + # No matching delta was accumulated. Keep the + # normal merge path so a completion-only event + # still carries its arguments exactly once. + _, merged = self._merge_tool_call(tool_calls, event.tool_call) + if key in completed_tool_calls: + continue + validate_complete_tool_call(merged, "Google Gemini") + completed_tool_calls.add(key) + event.tool_call = dict(merged) + yield event + continue + yield event + if not terminal_seen: + raise RuntimeError("Google Gemini 异步流在终止事件之前结束。") + return + result = await self.acomplete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + + def create_cache(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.create( + model=kwargs.pop("model", self._model_name), + **self._resource_config_kwargs(types.CreateCachedContentConfig, config, kwargs), + ) + + def get_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.get( + name=name, **self._resource_config_kwargs(types.GetCachedContentConfig, config, kwargs) + ) + + def list_caches(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.list( + **self._resource_config_kwargs(types.ListCachedContentsConfig, config, kwargs) + ) + + def update_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.update( + name=name, **self._resource_config_kwargs(types.UpdateCachedContentConfig, config, kwargs) + ) + + def delete_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.delete( + name=name, **self._resource_config_kwargs(types.DeleteCachedContentConfig, config, kwargs) + ) + + async def async_create_cache(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.create( + model=kwargs.pop("model", self._model_name), + **self._resource_config_kwargs(types.CreateCachedContentConfig, config, kwargs), + )) + + async def async_get_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.get( + name=name, **self._resource_config_kwargs(types.GetCachedContentConfig, config, kwargs) + )) + + async def async_list_caches(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.list( + **self._resource_config_kwargs(types.ListCachedContentsConfig, config, kwargs) + )) + + async def async_update_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.update( + name=name, **self._resource_config_kwargs(types.UpdateCachedContentConfig, config, kwargs) + )) + + async def async_delete_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.delete( + name=name, **self._resource_config_kwargs(types.DeleteCachedContentConfig, config, kwargs) + )) + + def create_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.create( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateBatchJobConfig, config, kwargs), + ) + + def create_embedding_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.create_embeddings( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateEmbeddingsBatchJobConfig, config, kwargs), + ) + + def get_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.get( + name=name, **self._resource_config_kwargs(types.GetBatchJobConfig, config, kwargs) + ) + + def list_batches(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.list( + **self._resource_config_kwargs(types.ListBatchJobsConfig, config, kwargs) + ) + + def cancel_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.cancel( + name=name, **self._resource_config_kwargs(types.CancelBatchJobConfig, config, kwargs) + ) + + def delete_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.delete( + name=name, **self._resource_config_kwargs(types.DeleteBatchJobConfig, config, kwargs) + ) + + async def async_create_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.create( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateBatchJobConfig, config, kwargs), + )) + + async def async_create_embedding_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.create_embeddings( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateEmbeddingsBatchJobConfig, config, kwargs), + )) + + async def async_get_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.get( + name=name, **self._resource_config_kwargs(types.GetBatchJobConfig, config, kwargs) + )) + + async def async_list_batches(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.list( + **self._resource_config_kwargs(types.ListBatchJobsConfig, config, kwargs) + )) + + async def async_cancel_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.cancel( + name=name, **self._resource_config_kwargs(types.CancelBatchJobConfig, config, kwargs) + )) + + async def async_delete_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.delete( + name=name, **self._resource_config_kwargs(types.DeleteBatchJobConfig, config, kwargs) + )) + + def list_models(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.list( + **self._resource_config_kwargs(types.ListModelsConfig, config, kwargs) + ) + + def get_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.get( + model=model, **self._resource_config_kwargs(types.GetModelConfig, config, kwargs) + ) + + def delete_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.delete( + model=model, **self._resource_config_kwargs(types.DeleteModelConfig, config, kwargs) + ) + + def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.update( + model=model, **self._resource_config_kwargs(types.UpdateModelConfig, config, kwargs) + ) + + def tune(self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any) -> Any: + """创建 Google GenAI 调优任务。""" + tunings = self._require_resource( + self._get_client(), "tunings", "调优" + ) + return tunings.tune( + base_model=base_model, + training_dataset=training_dataset, + **self._resource_config_kwargs(types.CreateTuningJobConfig, config, kwargs), + ) + + def get_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.get( + name=name, **self._resource_config_kwargs(types.GetTuningJobConfig, config, kwargs) + ) + + def list_tunings(self, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.list( + **self._resource_config_kwargs(types.ListTuningJobsConfig, config, kwargs) + ) + + def cancel_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.cancel( + name=name, **self._resource_config_kwargs(types.CancelTuningJobConfig, config, kwargs) + ) + + def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + if single_reward_config is not None and composite_reward_config is not None: + raise ValueError( + "Google validate_reward 的 single_reward_config 与 " + "composite_reward_config 不能同时传入。" + ) + reward_kwargs: dict[str, Any] = {} + if single_reward_config is not None: + reward_kwargs["single_reward_config"] = single_reward_config + if composite_reward_config is not None: + reward_kwargs["composite_reward_config"] = composite_reward_config + reward_kwargs.update( + self._resource_config_kwargs(types.ValidateRewardConfig, config, kwargs) + ) + return tunings.validate_reward( + parent=parent, + sample_response=sample_response, + example=example, + **reward_kwargs, + ) + + def recontext_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.recontext_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.RecontextImageConfig, config, kwargs), + ) + + async def async_list_models(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.list( + **self._resource_config_kwargs(types.ListModelsConfig, config, kwargs) + )) + + async def async_get_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.get( + model=model, **self._resource_config_kwargs(types.GetModelConfig, config, kwargs) + )) + + async def async_delete_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.delete( + model=model, **self._resource_config_kwargs(types.DeleteModelConfig, config, kwargs) + )) + + async def async_update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.update( + model=model, **self._resource_config_kwargs(types.UpdateModelConfig, config, kwargs) + )) + + async def async_tune(self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.tune( + base_model=base_model, + training_dataset=training_dataset, + **self._resource_config_kwargs(types.CreateTuningJobConfig, config, kwargs), + )) + + async def async_get_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.get( + name=name, **self._resource_config_kwargs(types.GetTuningJobConfig, config, kwargs) + )) + + async def async_list_tunings(self, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.list( + **self._resource_config_kwargs(types.ListTuningJobsConfig, config, kwargs) + )) + + async def async_cancel_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.cancel( + name=name, **self._resource_config_kwargs(types.CancelTuningJobConfig, config, kwargs) + )) + + async def async_validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + if single_reward_config is not None and composite_reward_config is not None: + raise ValueError( + "Google validate_reward 的 single_reward_config 与 " + "composite_reward_config 不能同时传入。" + ) + reward_kwargs: dict[str, Any] = {} + if single_reward_config is not None: + reward_kwargs["single_reward_config"] = single_reward_config + if composite_reward_config is not None: + reward_kwargs["composite_reward_config"] = composite_reward_config + reward_kwargs.update( + self._resource_config_kwargs(types.ValidateRewardConfig, config, kwargs) + ) + return await self._resolve_async_result(tunings.validate_reward( + parent=parent, + sample_response=sample_response, + example=example, + **reward_kwargs, + )) + + async def async_recontext_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.recontext_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.RecontextImageConfig, config, kwargs), + )) + + def generate_images(self, prompt: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.generate_images( + model=model or self._model_name, prompt=prompt, + **self._resource_config_kwargs(types.GenerateImagesConfig, config, kwargs), + ) + + def edit_image(self, prompt: str, reference_images: list[Any], model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.edit_image( + model=model or self._model_name, prompt=prompt, reference_images=reference_images, + **self._resource_config_kwargs(types.EditImageConfig, config, kwargs), + ) + + def upscale_image(self, image: Any, upscale_factor: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.upscale_image( + model=model or self._model_name, image=image, upscale_factor=upscale_factor, + **self._resource_config_kwargs(types.UpscaleImageConfig, config, kwargs), + ) + + def segment_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.segment_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.SegmentImageConfig, config, kwargs), + ) + + async def async_generate_images(self, prompt: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.generate_images( + model=model or self._model_name, prompt=prompt, + **self._resource_config_kwargs(types.GenerateImagesConfig, config, kwargs), + )) + + async def async_edit_image(self, prompt: str, reference_images: list[Any], model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.edit_image( + model=model or self._model_name, + prompt=prompt, + reference_images=reference_images, + **self._resource_config_kwargs(types.EditImageConfig, config, kwargs), + )) + + async def async_upscale_image(self, image: Any, upscale_factor: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.upscale_image( + model=model or self._model_name, + image=image, + upscale_factor=upscale_factor, + **self._resource_config_kwargs(types.UpscaleImageConfig, config, kwargs), + )) + + async def async_segment_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.segment_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.SegmentImageConfig, config, kwargs), + )) + + def generate_videos( + self, + prompt: str | None = None, + model: str | None = None, + image: Any = None, + video: Any = None, + source: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().models.generate_videos( + model=model or self._model_name, prompt=prompt, image=image, video=video, source=source, + **self._resource_config_kwargs(types.GenerateVideosConfig, config, kwargs), + ) + + def get_operation(self, operation: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().operations.get( + operation, **self._resource_config_kwargs(types.GetOperationConfig, config, kwargs) + ) + + async def async_generate_videos( + self, + prompt: str | None = None, + model: str | None = None, + image: Any = None, + video: Any = None, + source: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.generate_videos( + model=model or self._model_name, prompt=prompt, image=image, video=video, source=source, + **self._resource_config_kwargs(types.GenerateVideosConfig, config, kwargs), + )) + + async def async_get_operation(self, operation: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.operations.get( + operation, **self._resource_config_kwargs(types.GetOperationConfig, config, kwargs) + )) + + def create_chat( + self, + history: list[Any] | None = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().chats.create( + model=kwargs.pop("model", self._model_name), + history=history, + **self._resource_config_kwargs(types.GenerateContentConfig, config, kwargs), + ) + + async def async_create_chat( + self, + history: list[Any] | None = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + # google-genai returns an AsyncChat object synchronously; subsequent + # send_message calls on that object are awaitable. + return await self._resolve_async_result(self._get_client().aio.chats.create( + model=kwargs.pop("model", self._model_name), + history=history, + **self._resource_config_kwargs(types.GenerateContentConfig, config, kwargs), + )) + + def create_file_search_store(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.create( + **self._resource_config_kwargs(types.CreateFileSearchStoreConfig, config, kwargs) + ) + + def create_auth_token(self, config: Any = None, **kwargs: Any) -> Any: + """创建 Google GenAI Auth Token。""" + self._require_provider_resource("create_auth_token") + return self._get_client().auth_tokens.create( + **self._resource_config_kwargs(types.CreateAuthTokenConfig, config, kwargs) + ) + + # The next-generation Google resources are intentionally exposed as + # provider-specific methods. Their request/response types are experimental + # and should not be forced into the shared completion contract. + def create_interaction(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_interaction") + return self._get_client().interactions.create(**kwargs) + + def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_interaction") + return self._get_client().interactions.get(id=interaction_id, **kwargs) + + def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("cancel_interaction") + return self._get_client().interactions.cancel(id=interaction_id, **kwargs) + + def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_interaction") + return self._get_client().interactions.delete(id=interaction_id, **kwargs) + + def create_agent(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_agent") + return self._get_client().agents.create(**kwargs) + + def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_agent") + return self._get_client().agents.get(agent_id, **kwargs) + + def list_agents(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_agents") + return self._get_client().agents.list(**kwargs) + + def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_agent") + return self._get_client().agents.delete(agent_id, **kwargs) + + def create_webhook(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_webhook") + return self._get_client().webhooks.create(**kwargs) + + def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_webhook") + return self._get_client().webhooks.get(webhook_id, **kwargs) + + def list_webhooks(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_webhooks") + return self._get_client().webhooks.list(**kwargs) + + def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("update_webhook") + return self._get_client().webhooks.update(webhook_id, **kwargs) + + def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_webhook") + return self._get_client().webhooks.delete(webhook_id, **kwargs) + + def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("ping_webhook") + return self._get_client().webhooks.ping(webhook_id, **kwargs) + + def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("rotate_webhook_signing_secret") + return self._get_client().webhooks.rotate_signing_secret(webhook_id, **kwargs) + + def create_environment(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_environment") + return self._get_client().environments.create_environment(**kwargs) + + def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_environment") + return self._get_client().environments.get_environment(id=environment_id, **kwargs) + + def list_environments(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_environments") + return self._get_client().environments.list_environments(**kwargs) + + def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_environment") + return self._get_client().environments.delete_environment(id=environment_id, **kwargs) + + def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + """列出环境快照中的文件或目录内容。""" + self._require_provider_resource("get_environment_files") + return self._get_client().environments.files.list(environment_id, path, **kwargs) + + def create_trigger(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_trigger") + return self._get_client().triggers.create(**kwargs) + + def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_trigger") + return self._get_client().triggers.get(trigger_id, **kwargs) + + def list_triggers(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_triggers") + return self._get_client().triggers.list(**kwargs) + + def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("update_trigger") + return self._get_client().triggers.update(trigger_id, **kwargs) + + def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_trigger") + return self._get_client().triggers.delete(trigger_id, **kwargs) + + def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("run_trigger") + return self._get_client().triggers.run(trigger_id, **kwargs) + + def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("list_trigger_executions") + return self._get_client().triggers.list_executions(trigger_id, **kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回 Google Live API 的异步上下文管理器。 + + google-genai 只在 ``client.aio.live`` 提供 Live 连接,不能把它伪装成 + 普通的 ``await`` 方法;调用方应使用 ``async with`` 管理会话生命周期。 + """ + self._validate_resource_config(config) + if not model: + model = self._model_name + client = self._get_client() + try: + aio = client.aio + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + "当前 google-genai SDK 未提供异步 Live 资源。" + ) from exc + live = self._require_resource(aio, "live", "Live") + connect = self._require_resource(live, "connect", "Live 连接") + if not callable(connect): + raise NotImplementedError( + "当前 google-genai SDK 未提供可调用的 Live 连接资源。" + ) + return connect(model=model, config=config) + + def async_connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回异步 Live API 上下文管理器,调用方使用 `async with`。""" + self._validate_resource_config(config) + return self.connect_live(model=model, config=config) + + def get_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.get( + name=name, **self._resource_config_kwargs(types.GetFileSearchStoreConfig, config, kwargs) + ) + + def list_file_search_stores(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.list( + **self._resource_config_kwargs(types.ListFileSearchStoresConfig, config, kwargs) + ) + + def delete_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.delete( + name=name, **self._resource_config_kwargs(types.DeleteFileSearchStoreConfig, config, kwargs) + ) + + def import_file_to_file_search_store( + self, + file_search_store_name: str, + file_name: str, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().file_search_stores.import_file( + file_search_store_name=file_search_store_name, + file_name=file_name, + **self._resource_config_kwargs(types.ImportFileConfig, config, kwargs), + ) + + def upload_to_file_search_store(self, file_search_store_name: str, file: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.upload_to_file_search_store( + file_search_store_name=file_search_store_name, file=file, + **self._resource_config_kwargs(types.UploadToFileSearchStoreConfig, config, kwargs), + ) + + def download_file_search_media(self, media_id: str, config: Any = None, **kwargs: Any) -> bytes: + return self._get_client().file_search_stores.download_media( + media_id=media_id, **self._resource_config_kwargs(types.DownloadMediaConfig, config, kwargs) + ) + + async def async_create_file_search_store(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.create( + **self._resource_config_kwargs(types.CreateFileSearchStoreConfig, config, kwargs) + )) + + async def async_create_auth_token(self, config: Any = None, **kwargs: Any) -> Any: + """异步创建 Google GenAI Auth Token。""" + self._require_provider_resource("async_create_auth_token") + return await self._resolve_async_result(self._get_client().aio.auth_tokens.create( + **self._resource_config_kwargs(types.CreateAuthTokenConfig, config, kwargs) + )) + + async def async_create_interaction(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.create(**kwargs)) + + async def async_get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.get(id=interaction_id, **kwargs)) + + async def async_cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_cancel_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.cancel(id=interaction_id, **kwargs)) + + async def async_delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.delete(id=interaction_id, **kwargs)) + + async def async_create_agent(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_agent") + return await self._resolve_async_result(self._get_client().aio.agents.create(**kwargs)) + + async def async_get_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_agent") + return await self._resolve_async_result(self._get_client().aio.agents.get(agent_id, **kwargs)) + + async def async_list_agents(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_agents") + return await self._resolve_async_result(self._get_client().aio.agents.list(**kwargs)) + + async def async_delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_agent") + return await self._resolve_async_result(self._get_client().aio.agents.delete(agent_id, **kwargs)) + + async def async_create_webhook(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.create(**kwargs)) + + async def async_get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.get(webhook_id, **kwargs)) + + async def async_list_webhooks(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_webhooks") + return await self._resolve_async_result(self._get_client().aio.webhooks.list(**kwargs)) + + async def async_update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_update_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.update(webhook_id, **kwargs)) + + async def async_delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.delete(webhook_id, **kwargs)) + + async def async_ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_ping_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.ping(webhook_id, **kwargs)) + + async def async_rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_rotate_webhook_signing_secret") + return await self._resolve_async_result( + self._get_client().aio.webhooks.rotate_signing_secret(webhook_id, **kwargs) + ) + + async def async_create_environment(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.create_environment(**kwargs) + ) + + async def async_get_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.get_environment(id=environment_id, **kwargs) + ) + + async def async_list_environments(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_environments") + return await self._resolve_async_result( + self._get_client().aio.environments.list_environments(**kwargs) + ) + + async def async_delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.delete_environment(id=environment_id, **kwargs) + ) + + async def async_get_environment_files( + self, environment_id: str, path: str, **kwargs: Any + ) -> Any: + """异步列出环境快照中的文件或目录内容。""" + self._require_provider_resource("async_get_environment_files") + return await self._resolve_async_result( + self._get_client().aio.environments.files.list(environment_id, path, **kwargs) + ) + + async def async_create_trigger(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.create(**kwargs)) + + async def async_get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.get(trigger_id, **kwargs)) + + async def async_list_triggers(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_triggers") + return await self._resolve_async_result(self._get_client().aio.triggers.list(**kwargs)) + + async def async_update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_update_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.update(trigger_id, **kwargs)) + + async def async_delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.delete(trigger_id, **kwargs)) + + async def async_run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_run_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.run(trigger_id, **kwargs)) + + async def async_list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_trigger_executions") + return await self._resolve_async_result( + self._get_client().aio.triggers.list_executions(trigger_id, **kwargs) + ) + + async def async_get_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.get( + name=name, **self._resource_config_kwargs(types.GetFileSearchStoreConfig, config, kwargs) + )) + + async def async_list_file_search_stores(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.list( + **self._resource_config_kwargs(types.ListFileSearchStoresConfig, config, kwargs) + )) + + async def async_delete_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.delete( + name=name, **self._resource_config_kwargs(types.DeleteFileSearchStoreConfig, config, kwargs) + )) + + async def async_import_file_to_file_search_store( + self, + file_search_store_name: str, + file_name: str, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.import_file( + file_search_store_name=file_search_store_name, + file_name=file_name, + **self._resource_config_kwargs(types.ImportFileConfig, config, kwargs), + )) + + async def async_upload_to_file_search_store(self, file_search_store_name: str, file: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.upload_to_file_search_store( + file_search_store_name=file_search_store_name, file=file, + **self._resource_config_kwargs(types.UploadToFileSearchStoreConfig, config, kwargs), + )) + + async def async_download_file_search_media(self, media_id: str, config: Any = None, **kwargs: Any) -> bytes: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.download_media( + media_id=media_id, + **self._resource_config_kwargs(types.DownloadMediaConfig, config, kwargs), + )) + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = None, - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + n: int | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, ) -> Generator[str, None, None]: """同步调用 Google LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"调用 Google LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - config = types.GenerateContentConfig(system_instruction=system_prompt, temperature=temperature) + effective_system_prompt = extract_system_prompt(messages) or system_prompt + config = self._build_generation_config(effective_system_prompt, tools, temperature, max_tokens=max_tokens, + top_p=top_p, top_k=top_k, candidate_count=n, + frequency_penalty=frequency_penalty, + presence_penalty=presence_penalty, seed=seed, + modalities=modalities, stop=stop, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, + extra_headers=extra_headers, extra_query=extra_query, + timeout=timeout, + thinking=thinking, service_tier=service_tier) + contents = self._contents( + messages, + prompt, + system_prompt, + vertex_mode=self._effective_vertex_mode(client), + ) try: if stream: - response = client.models.generate_content_stream(model=self._model_name, contents=prompt, config=config) - for chunk in response: - if chunk.text: - yield chunk.text + terminal_seen = False + for chunk in retry_sync_stream( + lambda: client.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ): + raise_for_stream_error_event(chunk, "Google Gemini") + terminal_seen = terminal_seen or self._stream_chunk_has_terminal_event(chunk) + refusal = self._extract_refusal(chunk) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(chunk) + if text: + yield text + if not terminal_seen: + raise RuntimeError("Google Gemini 流在终止事件之前结束。") else: - response = client.models.generate_content(model=self._model_name, contents=prompt, config=config) - if response.text: - yield response.text + response = retry_sync_call( + lambda: client.models.generate_content( + model=self._model_name, contents=contents, config=config + ) + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(response) + if text: + yield text duration = time.perf_counter() - start_time logger.info(f"Google LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Google LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google LLM (%s) 出错: %s", + self._model_name, + error_text, + ) raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = None, - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + n: int | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, ) -> AsyncGenerator[str, None]: """异步调用 Google LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"异步调用 Google LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - config = types.GenerateContentConfig(system_instruction=system_prompt, temperature=temperature) + effective_system_prompt = extract_system_prompt(messages) or system_prompt + config = self._build_generation_config(effective_system_prompt, tools, temperature, max_tokens=max_tokens, + top_p=top_p, top_k=top_k, candidate_count=n, + frequency_penalty=frequency_penalty, + presence_penalty=presence_penalty, seed=seed, + modalities=modalities, stop=stop, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, + extra_headers=extra_headers, extra_query=extra_query, + timeout=timeout, + thinking=thinking, service_tier=service_tier) + contents = self._contents( + messages, + prompt, + system_prompt, + vertex_mode=self._effective_vertex_mode(client), + ) try: if stream: - response = await client.aio.models.generate_content_stream(model=self._model_name, contents=prompt, config=config) - async for chunk in response: - if chunk.text: - yield chunk.text + terminal_seen = False + async for chunk in retry_async_stream( + lambda: client.aio.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ): + raise_for_stream_error_event(chunk, "Google Gemini") + terminal_seen = terminal_seen or self._stream_chunk_has_terminal_event(chunk) + refusal = self._extract_refusal(chunk) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(chunk) + if text: + yield text + if not terminal_seen: + raise RuntimeError("Google Gemini 异步流在终止事件之前结束。") else: - response = await client.aio.models.generate_content(model=self._model_name, contents=prompt, config=config) - if response.text: - yield response.text + response = await retry_async_call(lambda: client.aio.models.generate_content( + model=self._model_name, contents=contents, config=config + )) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(response) + if text: + yield text duration = time.perf_counter() - start_time logger.info(f"Google LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Google LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google LLM (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步向量化文档。""" logger.info(f"调用 Google Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() try: - response = client.models.embed_content( - model=self._model_name, - contents=texts, - config=types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT") - ) - embeddings = [list(e.values) for e in response.embeddings] + config_values = self._embedding_options() + overlap = sorted({"model", "contents", "config"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Google Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request_options = { + key: kwargs.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + self._validate_embedding_kwargs(kwargs) + config_values.update(kwargs) + config_values.setdefault("task_type", "RETRIEVAL_DOCUMENT") + http_options = self._http_options_for_request(**request_options) + if http_options is not None: + config_values["http_options"] = http_options + config = types.EmbedContentConfig(**config_values) + # Validate and construct the complete request before initializing + # the SDK client, so invalid request extensions fail closed. + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is True and self._vertex_embed_content_only(): + embeddings = [] + for text in texts: + response = client.models.embed_content( + model=self._model_name, contents=[text], config=config + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != 1: + raise RuntimeError( + "Google Vertex Embedding 单内容请求未返回唯一 embedding。" + ) + embeddings.append(normalize_embedding_vector(field(values[0], "values"))) + else: + response = client.models.embed_content( + model=self._model_name, contents=texts, config=config + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != len(texts): + raise RuntimeError( + "Google Embedding 返回数量与输入文本数量不一致。" + ) + embeddings = [normalize_embedding_vector(field(item, "values")) for item in values] duration = time.perf_counter() - start_time logger.info(f"Google Embedding ({self._model_name}) 完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"Google Embedding ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google Embedding (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步向量化文档。""" logger.info(f"异步调用 Google Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() try: - response = await client.aio.models.embed_content( - model=self._model_name, - contents=texts, - config=types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT") - ) - embeddings = [list(e.values) for e in response.embeddings] + config_values = self._embedding_options() + overlap = sorted({"model", "contents", "config"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Google Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request_options = { + key: kwargs.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + self._validate_embedding_kwargs(kwargs) + config_values.update(kwargs) + config_values.setdefault("task_type", "RETRIEVAL_DOCUMENT") + http_options = self._http_options_for_request(**request_options) + if http_options is not None: + config_values["http_options"] = http_options + config = types.EmbedContentConfig(**config_values) + # Keep async embedding subject to the same fail-closed ordering as + # the sync entry point. + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is True and self._vertex_embed_content_only(): + embeddings = [] + for text in texts: + response = await self._resolve_async_result( + client.aio.models.embed_content( + model=self._model_name, contents=[text], config=config + ) + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != 1: + raise RuntimeError( + "Google Vertex Embedding 单内容请求未返回唯一 embedding。" + ) + embeddings.append(normalize_embedding_vector(field(values[0], "values"))) + else: + response = await self._resolve_async_result( + client.aio.models.embed_content( + model=self._model_name, contents=texts, config=config + ) + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != len(texts): + raise RuntimeError( + "Google Embedding 返回数量与输入文本数量不一致。" + ) + embeddings = [normalize_embedding_vector(field(item, "values")) for item in values] duration = time.perf_counter() - start_time logger.info(f"Google Embedding ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"Google Embedding ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google Embedding (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise + + def embed_query(self, text: str, **kwargs: Any) -> list[float]: + kwargs.setdefault("task_type", "RETRIEVAL_QUERY") + return self.embed_documents([text], **kwargs)[0] + + async def aembed_query(self, text: str, **kwargs: Any) -> list[float]: + kwargs.setdefault("task_type", "RETRIEVAL_QUERY") + return (await self.aembed_documents([text], **kwargs))[0] + + def close(self) -> None: + """释放同步 Client 及其关联的异步资源。 + + google-genai 的 ``Client.close`` 不会关闭 ``Client.aio``;同步上下文 + 使用生命周期桥接器等待异步关闭,活动事件循环中则保留 Client 并 + 显式要求调用 ``aclose``。 + """ + client = self._client + if client is None: + return + first_error: Exception | None = None + try: + client.close() + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + try: + close_resource_sync(client.aio, "Google 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._client = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放 google-genai 的异步 Client,并在异常后清除状态。""" + client = self._client + self._client = None + if client is None: + return + first_error: Exception | None = None + try: + result = client.aio.aclose() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error diff --git a/src/providers/grok.py b/src/providers/grok.py index a37eaf2..f46b6a0 100644 --- a/src/providers/grok.py +++ b/src/providers/grok.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider class GrokProvider(OpenAICompatibleProvider): """Grok 模型提供商。""" - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="grok") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="grok", protocol=protocol, options=options) diff --git a/src/providers/jina.py b/src/providers/jina.py index 5804877..cc8bacf 100644 --- a/src/providers/jina.py +++ b/src/providers/jina.py @@ -1,13 +1,18 @@ -# -*- coding: utf-8 -*- +import math import time -from typing import Any, Dict, List, Tuple +from collections.abc import Mapping +from typing import Any -import requests -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential -from src.providers.__base__.model_provider import RerankModel +from src.providers.__base__.model_provider import ( + RerankModel, + is_retryable_error, + validate_secret_free_options, +) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import redact_sensitive_text, validate_secret_free_payload logger = get_module_logger(__name__) @@ -17,8 +22,16 @@ class JinaProvider(RerankModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str): + _OPTION_KEYS = frozenset( + {"return_documents", "max_chunks_per_doc", "late_chunking", "truncate", "embedding_type", "extra_body", "timeout"} + ) + + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + self._options = validate_secret_free_options(options, "Jina Rerank") + unknown = sorted(set(self._options).difference(self._OPTION_KEYS)) + if unknown: + raise ValueError("Jina Rerank options 包含不支持的字段: " + ", ".join(unknown)) settings = get_settings() self._api_key = settings.jina_api_key if not self._api_key: @@ -27,85 +40,190 @@ def __init__(self, model_name: str): self._base_url = "https://api.jina.ai/v1/rerank" logger.info(f"初始化 JinaProvider,模型: {model_name}") - def _get_headers(self) -> Dict[str, str]: + def _get_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - def _prepare_payload(self, query: str, documents: List[str], top_n: int) -> Dict[str, Any]: - return { + def _prepare_payload(self, query: str, documents: list[str], top_n: int) -> dict[str, Any]: + self._validate_inputs(query, documents, top_n) + payload: dict[str, Any] = { "query": query, "documents": documents, "model": self._model_name, "top_n": top_n } + options = dict(self._options) + options.pop("timeout", None) + extra_body = validate_secret_free_payload( + options.pop("extra_body", None), + "Jina Rerank", + "options.extra_body", + ) + overlap = sorted(set(payload).intersection(options)) + if overlap: + raise ValueError("Jina Rerank options 不允许覆盖请求字段: " + ", ".join(overlap)) + if extra_body: + option_overlap = sorted(set(options).intersection(extra_body)) + if option_overlap: + raise ValueError( + "Jina Rerank options 与 options.extra_body 重复: " + + ", ".join(option_overlap) + ) + body_overlap = sorted(set(payload).intersection(extra_body)) + if body_overlap: + raise ValueError( + "Jina Rerank options.extra_body 不允许覆盖请求字段: " + + ", ".join(body_overlap) + ) + options.update(dict(extra_body)) + payload.update(options) + return payload - def _parse_response(self, results: List[Dict], documents: List[str]) -> Tuple[List[int], List[float]]: + def _request_timeout(self) -> float: + timeout = self._options.get("timeout", 30.0) + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("Jina Rerank options.timeout 必须是正数。") + return float(timeout) + + @staticmethod + def _validate_inputs(query: str, documents: list[str], top_n: int) -> None: + if not isinstance(query, str) or not query.strip(): + raise ValueError("Jina Rerank query 必须是非空字符串。") + if not isinstance(documents, list) or not documents: + raise ValueError("Jina Rerank documents 必须是非空列表。") + if any(not isinstance(document, str) for document in documents): + raise ValueError("Jina Rerank documents 必须全部是字符串。") + if not isinstance(top_n, int) or isinstance(top_n, bool) or top_n < 1: + raise ValueError("Jina Rerank top_n 必须是大于等于 1 的整数。") + + def _parse_response(self, results: list[dict], documents: list[str]) -> tuple[list[int], list[float]]: + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") # 创建内容到索引的映射 - content_to_index = {content: i for i, content in enumerate(documents)} + positions: dict[str, list[int]] = {} + for index, content in enumerate(documents): + positions.setdefault(content, []).append(index) indices = [] scores = [] for res in results: - doc_content = res.get("document", {}).get("text") if isinstance(res.get("document"), dict) else res.get("document") - if doc_content in content_to_index: - indices.append(content_to_index[doc_content]) - scores.append(res.get("relevance_score", 0.0)) + if not isinstance(res, Mapping): + raise RuntimeError("Jina Rerank 响应包含无效结果项。") + if "index" in res: + direct_index = res["index"] + if ( + not isinstance(direct_index, int) + or isinstance(direct_index, bool) + or not 0 <= direct_index < len(documents) + ): + raise RuntimeError("Jina Rerank 响应 index 无效。") + if direct_index in indices: + raise RuntimeError("Jina Rerank 响应包含重复 index。") + index = direct_index + document = res.get("document") + document_text = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if document_text is not None and document_text != documents[index]: + raise RuntimeError("Jina Rerank 响应 index 与 document 不匹配。") + positions.get(documents[index], []).remove(index) + else: + document = res.get("document") + doc_content = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if not isinstance(doc_content, str) or not positions.get(doc_content): + raise RuntimeError("Jina Rerank 响应无法映射到输入文档。") + index = positions[doc_content].pop(0) + try: + score = float(res["relevance_score"]) + except (KeyError, TypeError, ValueError) as exc: + raise RuntimeError("Jina Rerank 响应 relevance_score 无效。") from exc + if not math.isfinite(score): + raise RuntimeError("Jina Rerank 响应 relevance_score 必须是有限数值。") + indices.append(index) + scores.append(score) return indices, scores @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步 Rerank (CSE Sensor)。""" logger.info(f"调用 Jina Rerank ({self._model_name}),文档数: {len(documents)}") import httpx start_time = time.perf_counter() try: - with httpx.Client(timeout=30.0) as client: + with httpx.Client(timeout=self._request_timeout()) as client: response = client.post( self._base_url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("Jina Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"Jina Rerank ({self._model_name}) 完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"Jina Rerank ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Jina Rerank (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步 Rerank (CSE Sensor)。""" logger.info(f"异步调用 Jina Rerank ({self._model_name}),文档数: {len(documents)}") import httpx start_time = time.perf_counter() try: - async with httpx.AsyncClient(timeout=30.0) as aclient: + async with httpx.AsyncClient(timeout=self._request_timeout()) as aclient: response = await aclient.post( self._base_url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("Jina Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"Jina Rerank ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"Jina Rerank ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Jina Rerank (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/lm_studio.py b/src/providers/lm_studio.py index 080a213..ba99b45 100644 --- a/src/providers/lm_studio.py +++ b/src/providers/lm_studio.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class LMStudioProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="lm-studio") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="lm-studio", protocol=protocol, options=options) diff --git a/src/providers/local_hash.py b/src/providers/local_hash.py index 20690ae..01718ed 100644 --- a/src/providers/local_hash.py +++ b/src/providers/local_hash.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- import asyncio import hashlib import math import re -from typing import List -import jieba +import jieba # type: ignore[import-untyped] from src.providers.__base__.model_provider import TextEmbeddingModel @@ -20,12 +18,12 @@ def _parse_dimension(model_name: str) -> int: return int(suffix) if suffix.isdigit() and int(suffix) > 0 else DEFAULT_DIMENSION -def _tokenize(text: str) -> List[str]: +def _tokenize(text: str) -> list[str]: tokens = [token.strip() for token in jieba.lcut(text) if token.strip()] return tokens if tokens else TOKEN_PATTERN.findall(text) -def _normalize(vector: List[float]) -> List[float]: +def _normalize(vector: list[float]) -> list[float]: norm = math.sqrt(sum(value * value for value in vector)) if norm == 0: return vector @@ -39,7 +37,7 @@ def __init__(self, model_name: str): self.model_name = model_name self.dimension = _parse_dimension(model_name) - def _embed_text(self, text: str) -> List[float]: + def _embed_text(self, text: str) -> list[float]: vector = [0.0] * self.dimension for token in _tokenize(text): digest = hashlib.blake2b(token.encode("utf-8"), digest_size=16).digest() @@ -51,8 +49,18 @@ def _embed_text(self, text: str) -> List[float]: vector[secondary_index] += secondary_sign return _normalize(vector) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + @staticmethod + def _validate_kwargs(kwargs: dict) -> None: + if kwargs: + unsupported = ", ".join(sorted(str(key) for key in kwargs)) + raise ValueError( + "LocalHash Embedding 不支持请求参数: " + unsupported + ) + + def embed_documents(self, texts: list[str], **kwargs) -> list[list[float]]: + self._validate_kwargs(kwargs) return [self._embed_text(text) for text in texts] - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs) -> list[list[float]]: + self._validate_kwargs(kwargs) return await asyncio.to_thread(self.embed_documents, texts) diff --git a/src/providers/ollama.py b/src/providers/ollama.py index 8e485f0..da85710 100644 --- a/src/providers/ollama.py +++ b/src/providers/ollama.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class OllamaProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="ollama") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="ollama", protocol=protocol, options=options) diff --git a/src/providers/openai.py b/src/providers/openai.py index a05b88e..b9c137d 100644 --- a/src/providers/openai.py +++ b/src/providers/openai.py @@ -1,173 +1,44 @@ -# -*- coding: utf-8 -*- -import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from typing import Any -import openai -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type - -from src.providers.__base__.model_provider import ( - LargeLanguageModel, - TextEmbeddingModel, -) +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import AsyncOpenAIResources, OpenAIResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger logger = get_module_logger(__name__) -class OpenAIProvider(LargeLanguageModel, TextEmbeddingModel): - """ - OpenAI模型提供商,统一处理GPT LLM和Embedding。 - 已增加 tenacity 重试机制与性能传感器。 - """ - - def __init__(self, model_name: str): - self._model_name = model_name - settings = get_settings() - self._api_key = settings.openai_api_key - self._base_url = settings.openai_api_base - - if not self._api_key: - logger.error("OpenAI配置不完整:缺少 OPENAI_API_KEY。") - raise ValueError("OpenAI配置不完整:缺少 OPENAI_API_KEY。") - - self._client: Optional[openai.OpenAI] = None - self._aclient: Optional[openai.AsyncOpenAI] = None - logger.info(f"OpenAIProvider 初始化成功,模型: {model_name}") - - def _get_client(self) -> openai.OpenAI: - if self._client is None: - self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url) - return self._client - def _get_aclient(self) -> openai.AsyncOpenAI: - if self._aclient is None: - self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) - return self._aclient +class OpenAIProvider(OpenAICompatibleProvider): + """OpenAI 官方渠道,复用 OpenAI 兼容协议适配器。""" - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + capabilities = OpenAICompatibleProvider.capabilities | frozenset( + { + "files", "batches", "vector_stores", "responses", "moderation", + "audio", "images", "videos", "uploads", "parse", "conversations", + "containers", "fine_tuning", "evals", "skills", "realtime", + "webhooks", "admin", "content_provenance_checks", + } ) - def invoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> Generator[str, None, None]: - """同步调用 OpenAI LLM (CSE Sensor)。""" - logger.info(f"调用 OpenAI LLM ({self._model_name}),流式: {stream}") - client = self._get_client() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"OpenAI LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"OpenAI LLM ({self._model_name}) 出错: {e}", exc_info=True) - raise - - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + _resource_capabilities = frozenset( + { + "files", "batches", "vector_stores", "models", "moderation", + "images", "audio", "videos", "uploads", "conversations", + "containers", "fine_tuning", "evals", + } ) - async def ainvoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: - """异步调用 OpenAI LLM (CSE Sensor)。""" - logger.info(f"异步调用 OpenAI LLM ({self._model_name}),流式: {stream}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"OpenAI LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"OpenAI LLM ({self._model_name}) 异步出错: {e}", exc_info=True) - raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: - """同步向量化文档。""" - logger.info(f"调用 OpenAI Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() - start_time = time.perf_counter() - - try: - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"OpenAI Embedding ({self._model_name}) 完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"OpenAI Embedding ({self._model_name}) 出错: {e}", exc_info=True) - raise + @property + def resources(self) -> OpenAIResources: + return OpenAIResources(self) - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: - """异步向量化文档。""" - logger.info(f"异步调用 OpenAI Embedding ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"OpenAI Embedding ({self._model_name}) 异步完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"OpenAI Embedding ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + @property + def async_resources(self) -> AsyncOpenAIResources: + return AsyncOpenAIResources(self) + + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + # 保留本模块的配置入口,方便调用方在测试或运行时注入 settings。 + settings = get_settings() + if not settings.openai_api_key: + logger.error("OpenAI配置不完整:缺少 OPENAI_API_KEY。") + raise ValueError("OpenAI配置不完整:缺少 OPENAI_API_KEY。") + super().__init__(model_name=model_name, provider="openai", settings=settings, protocol=protocol, options=options) diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py index fbcd0ea..a4da675 100644 --- a/src/providers/openai_compatible.py +++ b/src/providers/openai_compatible.py @@ -1,16 +1,53 @@ -# -*- coding: utf-8 -*- +import asyncio +import inspect +import math import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from collections.abc import AsyncGenerator, AsyncIterator, Generator, Iterator, Mapping +from typing import Any, cast +from urllib.parse import urlsplit import openai -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + content_to_text, + is_retryable_error, + merge_tool_call_fragment, + normalize_embedding_vector, + normalize_messages, + normalize_responses_input, + normalize_tool_arguments, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, + validate_secret_free_options, + validate_secret_free_request_overrides, +) +from src.providers.resources import ( + AsyncOpenAICompatibleResources, + OpenAICompatibleResources, ) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) logger = get_module_logger(__name__) @@ -20,16 +57,126 @@ class OpenAICompatibleProvider(LargeLanguageModel, TextEmbeddingModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str, provider: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", + "structured_output", "usage", "embedding", "responses", + } + ) + # OpenAI's resource methods live on this shared adapter for code reuse, but + # only the official provider (or an explicitly verified Responses endpoint) + # may expose them. Keeping the declaration separate from the general + # capability set prevents compatible channels from accidentally advertising + # or dispatching official-only resources. + _resource_capabilities: frozenset[str] = frozenset() + # Tests and lightweight integrations sometimes instantiate this shared + # adapter directly with ``provider="openai"`` instead of using the + # OpenAIProvider subclass. Keep that explicit provider identity + # equivalent to the official adapter for resource gating, while leaving + # all other compatible providers fail-closed. + _OFFICIAL_OPENAI_RESOURCE_CAPABILITIES = frozenset( + { + "files", "batches", "vector_stores", "models", "moderation", + "images", "audio", "videos", "uploads", "conversations", + "containers", "fine_tuning", "evals", + } + ) + # These fields are accepted by OpenAI Chat Completions but are not part of + # the Responses request shape. Keep this guard protocol-specific: the + # current OpenAI SDK also exposes the fields below on ``responses.create`` + # (with ``verbosity`` normalized into ``text`` by this adapter). + _OPENAI_ONLY_CHAT_FIELDS = frozenset( + { + "modalities", + "audio", + "prediction", + "web_search_options", + } + ) + _OPENAI_ONLY_RESPONSES_FIELDS: frozenset[str] = frozenset() + _OPENAI_ONLY_CHAT_COMPAT_FIELDS = frozenset( + { + "store", + "prompt_cache_key", + "prompt_cache_options", + "prompt_cache_retention", + "safety_identifier", + "moderation", + "verbosity", + } + ) + _BACKGROUND_OPTION_KEYS = frozenset( + { + "background", + "background_poll_interval", + "background_timeout", + "responses_poll_interval", + "responses_poll_timeout", + } + ) + # Model-level options are defaults for the corresponding SDK request. Keep + # the allow-list separate from request-level ``extra_body`` so a typo cannot + # reach the OpenAI SDK as an unknown keyword argument. + _CHAT_MODEL_OPTION_KEYS = frozenset( + { + "temperature", "max_tokens", "max_completion_tokens", "top_p", + "frequency_penalty", "presence_penalty", "n", "logit_bias", + "logprobs", "top_logprobs", "modalities", "audio", "prediction", + "web_search_options", "seed", "stop", "response_format", + "tool_choice", "metadata", "user", "reasoning_effort", + "stream_options", "parallel_tool_calls", "store", "service_tier", + "prompt_cache_key", "prompt_cache_options", "prompt_cache_retention", + "safety_identifier", "moderation", "verbosity", "extra_body", + } + ) + _RESPONSES_MODEL_OPTION_KEYS = frozenset( + { + "background", "context_management", "conversation", "include", + "max_output_tokens", "max_tool_calls", "metadata", "moderation", + "parallel_tool_calls", "previous_response_id", "prompt", + "prompt_cache_key", "prompt_cache_options", "prompt_cache_retention", + "reasoning", "reasoning_effort", "safety_identifier", "service_tier", + "store", "stream_options", "temperature", "text", "tool_choice", + "tools", "top_logprobs", "top_p", "truncation", "user", + # Portable/common names normalized by this adapter. + "max_tokens", "max_completion_tokens", "response_format", "stop", "seed", + "verbosity", + "top_k", "frequency_penalty", "presence_penalty", "extra_body", + } + ) + _EMBEDDING_MODEL_OPTION_KEYS = frozenset( + {"dimensions", "encoding_format", "user", "extra_body"} + ) + _EMBEDDING_CLIENT_OPTION_KEYS = frozenset( + {"timeout", "max_retries", "server_verified_protocols"} + ) + + def __init__( + self, + model_name: str, + provider: str, + settings: Any | None = None, + protocol: str = "chat_completions", + options: dict[str, Any] | None = None, + ): self._model_name = model_name self._provider = provider - settings = get_settings() + self._protocol = self._normalize_protocol(protocol) + self._options = validate_secret_free_options(options, provider) + self._server_verified_protocols = self._normalize_verified_protocols( + self._options.get("server_verified_protocols", ()) + ) + settings = settings if settings is not None else get_settings() - api_key_name = f"{provider.lower()}_api_key" - base_url_name = f"{provider.lower()}_base_url" + settings_prefix = provider.lower().replace("-", "_") + api_key_name = f"{settings_prefix}_api_key" + base_url_name = f"{settings_prefix}_base_url" self._api_key = getattr(settings, api_key_name, None) self._base_url = getattr(settings, base_url_name, None) + if self._base_url is None: + # OpenAI 旧配置字段使用 `openai_api_base`,继续兼容现有配置文件。 + self._base_url = getattr(settings, f"{settings_prefix}_api_base", None) if provider in ["ollama", "lm-studio"] and not self._api_key: self._api_key = "no-key-required" @@ -37,133 +184,5006 @@ def __init__(self, model_name: str, provider: str): if not self._api_key: logger.error(f"{provider} API Key 未设置。") raise ValueError(f"{api_key_name} is required for {provider}") + if provider != "openai" and not self._base_url: + logger.error(f"{provider} Base URL 未设置。") + raise ValueError(f"{base_url_name} is required for {provider}") - self._client: Optional[openai.OpenAI] = None - self._aclient: Optional[openai.AsyncOpenAI] = None + self._client: openai.OpenAI | None = None + self._aclient: openai.AsyncOpenAI | None = None logger.info(f"初始化 OpenAICompatibleProvider ({provider}),模型: {model_name}") + @property + def resources(self) -> OpenAICompatibleResources: + return OpenAICompatibleResources(self) + + @property + def async_resources(self) -> AsyncOpenAICompatibleResources: + return AsyncOpenAICompatibleResources(self) + + @staticmethod + def _normalize_protocol(protocol: str) -> str: + normalized = str(protocol).strip().lower().replace("-", "_") + if normalized in {"chat", "completion", "chat_completion", "chat_completions"}: + return "chat_completions" + if normalized in {"response", "responses"}: + return "responses" + raise ValueError( + f"OpenAI 兼容渠道不支持 {protocol} 协议。可选值: chat_completions, responses" + ) + + @classmethod + def _normalize_verified_protocols(cls, configured: Any) -> frozenset[str]: + """规范化服务端协议登记,并在配置边界给出明确错误。""" + if configured is None: + return frozenset() + if isinstance(configured, str): + configured = (configured,) + elif not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError( + "server_verified_protocols 必须是字符串或协议序列。" + ) + normalized: set[str] = set() + for value in configured: + if not isinstance(value, str) or not value.strip(): + raise ValueError( + "server_verified_protocols 中的协议必须是非空字符串。" + ) + normalized.add(cls._normalize_protocol(value)) + return frozenset(normalized) + def _get_client(self) -> openai.OpenAI: if self._client is None: - self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url) + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url, **client_options) return self._client def _get_aclient(self) -> openai.AsyncOpenAI: if self._aclient is None: - self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url, **client_options) return self._aclient - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容 SDK 的协程方法和同步分页器返回值。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _validate_sdk_resource_call( + method: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], + operation: str, + ) -> None: + """在调用资源 SDK 前校验参数名和必填字段。 + + 官方 SDK 方法通常没有 ``**kwargs``,因此 ``Signature.bind`` 可以在 + 请求发出前同时拦截未知参数和缺失必填参数。测试替身或未来 SDK 若 + 暴露动态签名,则保留原有转发能力。 + """ + try: + signature = inspect.signature(method) + except (TypeError, ValueError): + return + try: + # Bind partially first so an unknown keyword is reported even when + # the same call also omits another required argument. This keeps + # the actionable caller error stable instead of depending on the + # order in which ``inspect.Signature`` enumerates parameters. + signature.bind_partial(*args, **dict(kwargs)) + signature.bind(*args, **dict(kwargs)) + except TypeError as exc: + raise ValueError(f"{operation} SDK 参数不匹配: {exc}") from exc + except ValueError as exc: + # A few extension/builtin signatures raise ValueError while + # binding positional-only arguments. Do not silently skip this + # validation boundary; surface the SDK contract failure instead. + raise ValueError(f"{operation} SDK 参数无法验证: {exc}") from exc + + def _call_sdk_resource( + self, + method: Any, + args: tuple[Any, ...] = (), + kwargs: Mapping[str, Any] | None = None, + operation: str = "OpenAI 资源", + ) -> Any: + """校验并调用一个 OpenAI SDK 资源方法。 + + Provider 资源入口保留 ``**kwargs`` 是为了兼容 SDK 的扩展字段; + 这里把实际 SDK 方法签名作为最后一道边界,确保未知参数和缺失的 + 必填参数在请求发出前转换成项目统一的 ``ValueError``。对无法 + introspect 的第三方替身仍保持原有转发能力。 + """ + call_args = validate_secret_free_resource_args( + args, + f"{getattr(self, '_provider', 'OpenAI')} {operation}", + ) + call_kwargs = validate_secret_free_resource_kwargs( + kwargs or {}, + f"{getattr(self, '_provider', 'OpenAI')} {operation}", + ) + self._validate_sdk_resource_call(method, call_args, call_kwargs, operation) + return method(*call_args, **call_kwargs) + + async def _call_async_sdk_resource( + self, + method: Any, + args: tuple[Any, ...] = (), + kwargs: Mapping[str, Any] | None = None, + operation: str = "OpenAI 资源", + ) -> Any: + """异步版本的 SDK 资源签名校验与调用。""" + return await self._resolve_async_result( + self._call_sdk_resource(method, args, kwargs, operation) + ) + + def _request_options(self) -> dict[str, Any]: + """返回调用方配置的受控扩展参数;凭证不会进入 ModelDetail.options。""" + validate_secret_free_options( + getattr(self, "_options", {}) or {}, + getattr(self, "_provider", "openai"), + ) + client_only = {"timeout", "max_retries", "server_verified_protocols"} + non_openai_top_level = {"top_k", "repetition_penalty"} + return { + key: value + for key, value in (getattr(self, "_options", {}) or {}).items() + if key not in client_only + and key not in non_openai_top_level + and key not in self._BACKGROUND_OPTION_KEYS + } + + @staticmethod + def _validate_model_options( + options: Mapping[str, Any], + allowed: frozenset[str], + endpoint: str, + ) -> None: + unknown = { + key: value + for key, value in options.items() + if key not in allowed + } + reject_unsupported_kwargs(endpoint, unknown) + + def _background_poll_config(self) -> tuple[float, float]: + options = getattr(self, "_options", {}) or {} + interval = options.get( + "background_poll_interval", + options.get("responses_poll_interval", 1.0), + ) + timeout = options.get( + "background_timeout", + options.get("responses_poll_timeout", 300.0), + ) + if ( + isinstance(interval, bool) + or not isinstance(interval, (int, float)) + or not math.isfinite(float(interval)) + or interval <= 0 + ): + raise ValueError("Responses background_poll_interval 必须是正数。") + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("Responses background_timeout 必须是正数。") + return float(interval), float(timeout) + + @classmethod + def _response_retrieve_kwargs(cls, params: Mapping[str, Any]) -> dict[str, Any]: + return { + key: params[key] + for key in ("include", "extra_headers", "extra_query", "extra_body", "timeout") + if key in params and params[key] is not None + } + + def _poll_background_response(self, response: Any, params: Mapping[str, Any]) -> Any: + status = self._field(response, "status") + if status not in {"queued", "in_progress", "pending"}: + return response + response_id = self._field(response, "id") + if not response_id: + raise RuntimeError("Responses background 响应缺少 id,无法轮询。") + interval, timeout = self._background_poll_config() + deadline = time.monotonic() + timeout + retrieve_kwargs = self._response_retrieve_kwargs(params) + while status in {"queued", "in_progress", "pending"}: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + f"Responses background 响应 {response_id} 在 {timeout:g}s 内未完成。" + ) + if interval: + time.sleep(min(interval, remaining)) + response = retry_sync_call( + lambda: self._get_client().responses.retrieve(response_id, **retrieve_kwargs) + ) + status = self._field(response, "status") + return response + + async def _poll_background_response_async(self, response: Any, params: Mapping[str, Any]) -> Any: + status = self._field(response, "status") + if status not in {"queued", "in_progress", "pending"}: + return response + response_id = self._field(response, "id") + if not response_id: + raise RuntimeError("Responses background 响应缺少 id,无法轮询。") + interval, timeout = self._background_poll_config() + deadline = time.monotonic() + timeout + retrieve_kwargs = self._response_retrieve_kwargs(params) + while status in {"queued", "in_progress", "pending"}: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + f"Responses background 响应 {response_id} 在 {timeout:g}s 内未完成。" + ) + if interval: + await asyncio.sleep(min(interval, remaining)) + response = await retry_async_call( + lambda: self._get_aclient().responses.retrieve(response_id, **retrieve_kwargs) + ) + status = self._field(response, "status") + return response + + def _configured_extra_body_keys(self) -> set[str]: + """返回模型级 extra_body 的显式字段名,用于冲突检测。""" + configured = (getattr(self, "_options", {}) or {}).get("extra_body") + if configured is None: + return set() + if not isinstance(configured, Mapping): + raise ValueError("Provider options.extra_body 必须是对象。") + return set(configured) + + def _compat_extra_options(self) -> dict[str, Any]: + return { + key: value + for key, value in (getattr(self, "_options", {}) or {}).items() + if key in {"top_k", "repetition_penalty"} + } + + def _validate_openai_field_compatibility( + self, values: Mapping[str, Any], endpoint: str, protocol: str | None = None + ) -> None: + """拒绝当前协议不支持或兼容端点未声明的 OpenAI 字段。""" + if self._uses_official_openai_endpoint(): + return + effective_protocol = protocol or getattr(self, "_protocol", "chat_completions") + if effective_protocol == "responses": + unsupported_fields = self._OPENAI_ONLY_RESPONSES_FIELDS + else: + unsupported_fields = ( + self._OPENAI_ONLY_CHAT_FIELDS | self._OPENAI_ONLY_CHAT_COMPAT_FIELDS + ) + unsupported = { + key: value + for key, value in values.items() + if key in unsupported_fields and value is not None + } + reject_unsupported_kwargs( + f"{getattr(self, '_provider', 'compatible')} {endpoint}", + unsupported, + ) + + def _validate_chat_field_compatibility(self, values: Mapping[str, Any]) -> None: + self._validate_openai_field_compatibility(values, "Chat Completions") + + @staticmethod + def _resource_capability_for_method(method_name: str) -> str | None: + """将 OpenAI 资源入口映射到统一的能力名称。""" + name = method_name.removeprefix("async_") + if name in { + "retrieve_response", + "cancel_response", + "delete_response", + "list_response_input_items", + "connect_responses", + "stream_responses", + "count_input_tokens", + "compact_responses", + }: + return "responses" + if name in { + "upload_file", + "list_files", + "retrieve_file", + "file_content", + "retrieve_file_content", + "delete_file", + "wait_for_file", + }: + return "files" + if name in { + "create_upload", + "complete_upload", + "cancel_upload", + "create_upload_part", + "upload_file_chunked", + }: + return "uploads" + if name == "text_to_speech": + return "audio" + prefixes = ( + ("vector_store", "vector_stores"), + ("fine_tuning", "fine_tuning"), + ("eval", "evals"), + ("conversation", "conversations"), + ("container", "containers"), + ("upload", "uploads"), + ("video", "videos"), + ("image", "images"), + ("audio", "audio"), + ("moderation", "moderation"), + ("model", "models"), + ("batch", "batches"), + ) + for prefix, capability in prefixes: + if name.startswith(prefix) or f"_{prefix}" in name: + return capability + return None + + def _require_provider_resource(self, method_name: str) -> None: + """在原生资源方法真正触达 SDK 前校验渠道能力。""" + capability = self._resource_capability_for_method(method_name) + if capability is None: + return + if capability == "responses": + self._require_responses_resource(method_name) + return + # An object created with ``object.__new__`` has no instance metadata; + # in that case there is no provider identity to gate and the SDK + # signature/resource checks below should remain testable. A real + # OpenAI provider is identified explicitly by ``_provider`` and gets + # the same resource set as the dedicated subclass. + provider_name = getattr(self, "_provider", None) + if provider_name is None: + return + declared: frozenset[str] = getattr(self, "_resource_capabilities", frozenset()) + if provider_name == "openai" and not declared: + declared = self._OFFICIAL_OPENAI_RESOURCE_CAPABILITIES + if capability not in declared: + raise NotImplementedError( + f"{provider_name} 未声明 {capability} 资源能力。" + ) + + def _chat_max_tokens_key(self) -> str: + """返回当前 Chat Completions 端点的输出长度字段名。""" + # OpenAI 官方 SDK 3.x 已将该字段更名为 max_completion_tokens。 + # 绝大多数兼容网关(Qwen、DeepSeek、Ollama、LM Studio、Grok、 + # SiliconFlow)仍按 Chat Completions 契约使用 max_tokens。 + provider = getattr(self, "_provider", "openai") + return ( + "max_completion_tokens" + if provider == "openai" and self._uses_official_openai_endpoint() + else "max_tokens" + ) + + def _embedding_options(self) -> dict[str, Any]: + """校验并返回 Embeddings 端点接受的模型级参数。 + + 聊天参数不能复用到 Embeddings,否则 OpenAI SDK 会在本地校验阶段 + 抛出 TypeError,兼容渠道也可能收到无效字段。客户端级配置仍可 + 放在同一份 provider options 中,但不会被转发为 Embeddings 请求字段。 + """ + options = getattr(self, "_options", {}) or {} + supported = self._EMBEDDING_MODEL_OPTION_KEYS + allowed = supported | self._EMBEDDING_CLIENT_OPTION_KEYS + reject_unsupported_kwargs( + f"{getattr(self, '_provider', 'OpenAI-compatible')} Embedding 模型 options", + {key: value for key, value in options.items() if key not in allowed}, + ) + return { + key: value + for key, value in options.items() + if key in supported + } + + @staticmethod + def _validate_embedding_kwargs(kwargs: Mapping[str, Any]) -> None: + allowed = { + "dimensions", + "encoding_format", + "user", + "extra_headers", + "extra_query", + "extra_body", + "timeout", + } + client_only = { + "base_url", + "default_headers", + "default_query", + "http_client", + "max_retries", + "server_verified_protocols", + } + unsupported = { + key: value + for key, value in kwargs.items() + if key not in allowed or key in client_only + } + reject_unsupported_kwargs("OpenAI Embedding", unsupported) + validate_secret_free_request_overrides( + kwargs.get("extra_headers"), + kwargs.get("extra_query"), + "OpenAI Embedding", + ) + + def _uses_official_openai_endpoint(self) -> bool: + """只把 OpenAI 官方 Responses 端点视为默认已知资源契约。 + + ``provider='openai'`` 也可用于代理或自建兼容网关,因此不能仅凭 + provider 名称放行 SDK 的专属资源。缺少 ``base_url`` 时沿用 SDK + 默认端点语义;显式 URL 则必须精确指向 ``https://api.openai.com/v1``。 + """ + base_url = getattr(self, "_base_url", None) + if not base_url: + # 只有 OpenAI Provider 省略 Base URL 时才会落到 SDK 官方默认端点。 + return getattr(self, "_provider", "openai") == "openai" + try: + parsed = urlsplit(str(base_url).strip()) + port = parsed.port + except ValueError: + return False + return ( + parsed.scheme.lower() == "https" + and parsed.hostname is not None + and parsed.hostname.lower() == "api.openai.com" + and (port in (None, 443)) + and parsed.path.rstrip("/") == "/v1" + and not parsed.query + and not parsed.fragment + ) + + def _require_responses_resource(self, operation: str) -> None: + """阻止兼容渠道把本地 SDK 资源误当成远端 Responses 能力。""" + if self._protocol != "responses": + raise ValueError(f"{operation} 仅适用于 responses 协议。") + # 官方端点由其 SDK 契约覆盖;代理、自建网关和其它兼容渠道必须 + # 显式声明已验证的服务端协议,避免本地 SDK 资源导致远端 404/405。 + provider = getattr(self, "_provider", "openai") + verified = "responses" in getattr(self, "_server_verified_protocols", ()) + if not verified and not self._uses_official_openai_endpoint(): + raise ValueError( + f"{provider} 未验证 Responses {operation} 的服务端能力;" + "请在 provider options 中显式配置 server_verified_protocols=['responses']。" + ) + + @staticmethod + def _merge_options(request: dict[str, Any], options: dict[str, Any]) -> None: + reserved = {"model", "messages", "input", "stream", "instructions", "tools"} + overlap = sorted(reserved.intersection(options)) + if overlap: + raise ValueError(f"Provider options 不允许覆盖请求字段: {', '.join(overlap)}") + # options 是模型级默认值;调用级字段在后续构造阶段覆盖它们。 + for key, value in options.items(): + request.setdefault(key, value) + + @staticmethod + def _validated_extra_body( + value: Any, + endpoint: str, + reserved: set[str], + ) -> dict[str, Any]: + """校验 SDK 的扩展请求体,避免字符串或保留字段穿透到请求边界。""" + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{endpoint} extra_body 必须是对象。") + value = validate_secret_free_payload(value, endpoint, "extra_body") + overlap = sorted(set(value).intersection(reserved)) + if overlap: + raise ValueError( + f"{endpoint} extra_body 不允许覆盖请求字段: {', '.join(overlap)}" + ) + return dict(value) + + @staticmethod + def _merge_extra_body( + configured: Mapping[str, Any] | None, + extensions: Mapping[str, Any] | None, + endpoint: str, + ) -> dict[str, Any]: + """合并模型级扩展,拒绝同一字段的静默覆盖。""" + configured_values = dict(configured or {}) + extension_values = dict(extensions or {}) + overlap = sorted(set(configured_values).intersection(extension_values)) + if overlap: + raise ValueError( + f"{endpoint} 模型 options 的扩展字段重复: {', '.join(overlap)}" + ) + return {**configured_values, **extension_values} + + @staticmethod + def _merge_resource_kwargs( + fixed: Mapping[str, Any], + overrides: Mapping[str, Any] | None, + operation: str, + *, + omit_none: bool = False, + ) -> dict[str, Any]: + """合并资源入口参数,拒绝调用方覆盖 Facade 已绑定的字段。""" + fixed_values = { + key: value + for key, value in fixed.items() + if not omit_none or value is not None + } + override_values = dict(overrides or {}) + overlap = sorted(set(fixed_values).intersection(override_values)) + if overlap: + raise ValueError( + f"{operation} 资源 SDK 参数重复: {', '.join(overlap)}" + ) + return {**fixed_values, **override_values} + + def _build_chat_request( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + stream: bool = True, + *, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + repetition_penalty: float | None = None, + n: int | None = None, + logit_bias: dict[str, int] | None = None, + logprobs: bool | None = None, + top_logprobs: int | None = None, + modalities: list[str] | None = None, + audio: dict[str, Any] | None = None, + prediction: dict[str, Any] | None = None, + web_search_options: dict[str, Any] | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + reasoning: dict[str, Any] | None = None, + stream_options: dict[str, Any] | None = None, + parallel_tool_calls: bool | None = None, + store: bool | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: dict[str, Any] | None = None, + prompt_cache_retention: str | None = None, + safety_identifier: str | None = None, + moderation: dict[str, Any] | None = None, + service_tier: str | None = None, + verbosity: str | None = None, + **ignored: Any, + ) -> dict[str, Any]: + reject_unsupported_kwargs("OpenAI Chat Completions", ignored) + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + f"{getattr(self, '_provider', 'openai')} Chat Completions", + ) + normalized_messages = normalize_messages(prompt, system_prompt, messages) + + request: dict[str, Any] = { + "model": self._model_name, + "messages": normalized_messages, + "stream": stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._CHAT_MODEL_OPTION_KEYS, + f"{getattr(self, '_provider', 'compatible')} Chat Completions", + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + if configured_max_tokens is not None and configured_max_completion_tokens is not None: + raise ValueError( + "Chat Completions options 不能同时设置 max_tokens 和 " + "max_completion_tokens。" + ) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Chat Completions", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_explicit_extra_keys = set(configured_extra_body) + self._validate_chat_field_compatibility(configured_options) + self._merge_options(request, configured_options) + configured_extra = self._compat_extra_options() + merged_configured_extra = self._merge_extra_body( + configured_extra_body, configured_extra, "Chat Completions" + ) + if merged_configured_extra: + request["extra_body"] = merged_configured_extra + if temperature is not None: + request["temperature"] = temperature + elif "temperature" not in request: + request["temperature"] = 0.7 + if tools: + request["tools"] = tools + max_tokens_key = self._chat_max_tokens_key() + if max_tokens is not None: + request[max_tokens_key] = max_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + elif configured_max_tokens is not None and max_tokens_key not in request: + request[max_tokens_key] = configured_max_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + elif configured_max_completion_tokens is not None and max_tokens_key not in request: + request[max_tokens_key] = configured_max_completion_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + if top_p is not None: + request["top_p"] = top_p + request_extensions = { + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + "n": n, + "logit_bias": logit_bias, + "logprobs": logprobs, + "top_logprobs": top_logprobs, + "modalities": modalities, + "audio": audio, + "prediction": prediction, + "web_search_options": web_search_options, + "parallel_tool_calls": parallel_tool_calls, + "store": store, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "safety_identifier": safety_identifier, + "moderation": moderation, + "service_tier": service_tier, + "verbosity": verbosity, + } + self._validate_chat_field_compatibility(request_extensions) + for key, value in request_extensions.items(): + if value is not None: + request[key] = value + if repetition_penalty is not None: + # repetition_penalty 是部分 OpenAI 兼容服务的扩展参数, + # OpenAI 官方 Chat Completions 不接受它作为顶层字段。 + if "repetition_penalty" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions repetition_penalty 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["repetition_penalty"] = repetition_penalty + if top_k is not None and self._protocol == "chat_completions": + if "top_k" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions top_k 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_explicit_extra_keys: + raise ValueError("Chat Completions seed 与模型 options.extra_body 重复。") + request["seed"] = seed + if stop is not None: + request["stop"] = stop + if response_format is not None: + request["response_format"] = response_format + if tool_choice is not None: + request["tool_choice"] = tool_choice + if metadata is not None: + request["metadata"] = metadata + if user is not None: + request["user"] = user + if timeout is not None: + request["timeout"] = timeout + if extra_headers is not None: + request["extra_headers"] = extra_headers + if extra_query is not None: + request["extra_query"] = extra_query + if reasoning is not None: + effort = reasoning.get("effort") if isinstance(reasoning, Mapping) else None + if effort is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions reasoning 与模型 options.extra_body 重复。" + ) + request["reasoning_effort"] = effort + else: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions reasoning 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["reasoning"] = reasoning + if stream_options is not None: + request["stream_options"] = stream_options + request_extra_body = self._validated_extra_body( + extra_body, + "Chat Completions", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Chat Completions extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Chat Completions", + set(request).difference({"extra_body"}), + ) + return request + + @staticmethod + def _convert_responses_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + if not tools: + return None + + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Responses 工具定义[{index}] 必须是对象。") + if "function" not in tool: + native_tool = dict(tool) + tool_type = native_tool.get("type") + if not isinstance(tool_type, str) or not tool_type.strip(): + raise ValueError( + f"Responses 工具定义[{index}] 缺少有效 type。" + ) + if tool_type == "function": + name = native_tool.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Responses 工具定义[{index}] 缺少 name。" + ) + parameters = native_tool.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].parameters 必须是对象。" + ) + native_tool["parameters"] = dict(parameters) + native_tool.setdefault("strict", False) + converted.append(native_tool) + continue + + function = tool["function"] + if not isinstance(function, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"Responses 工具定义[{index}] 缺少 function.name。") + parameters = function.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].function.parameters 必须是对象。" + ) + response_tool: dict[str, Any] = { + "type": "function", + "name": name, + "parameters": dict(parameters), + "strict": function.get("strict", tool.get("strict", False)), + } + if "description" in function: + response_tool["description"] = function["description"] + for key in ("allowed_callers", "defer_loading", "output_schema"): + if key in function: + response_tool[key] = function[key] + converted.append(response_tool) + return converted + + @staticmethod + def _convert_responses_tool_choice(tool_choice: Any) -> Any: + """将 Chat Completions 风格的工具选择转换为 Responses 形状。 + + Responses 的指定函数形式是扁平的 ``{type, name}``,而统一请求 + 也接受 ``{type: function, function: {name}}``。原生 Responses + 工具选择以及字符串模式保持不变,便于调用方直接使用 SDK 形状。 + """ + if tool_choice is None or isinstance(tool_choice, str): + return tool_choice + if not isinstance(tool_choice, Mapping): + raise ValueError("Responses tool_choice 必须是字符串或对象。") + + choice = dict(tool_choice) + if choice.get("type") != "function": + return choice + function = choice.get("function") + if isinstance(function, Mapping): + name = function.get("name") + if not name: + raise ValueError("Responses 的 function tool_choice 缺少 name。") + converted = {key: value for key, value in choice.items() if key != "function"} + converted["name"] = name + return converted + if isinstance(choice.get("name"), str) and choice["name"]: + return choice + raise ValueError("Responses 的 function tool_choice 缺少 name。") + + def _build_responses_request( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + stream: bool = True, + *, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + previous_response_id: str | None = None, + include: list[str] | None = None, + reasoning: dict[str, Any] | None = None, + store: bool | None = None, + background: bool | None = None, + parallel_tool_calls: bool | None = None, + max_tool_calls: int | None = None, + conversation: str | dict[str, Any] | None = None, + session: dict[str, Any] | None = None, + context_management: dict[str, Any] | None = None, + caching: dict[str, Any] | None = None, + expire_at: int | None = None, + prompt_cache_options: dict[str, Any] | None = None, + safety_identifier: str | None = None, + moderation: dict[str, Any] | None = None, + top_logprobs: int | None = None, + verbosity: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + truncation: str | None = None, + stream_options: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + **ignored: Any, + ) -> dict[str, Any]: + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + f"{getattr(self, '_provider', 'openai')} Responses", + ) + reject_unsupported_kwargs( + "OpenAI Responses", + { + "thinking": thinking, + "caching": caching, + "session": session, + "expire_at": expire_at, + }, + ) + reject_unsupported_kwargs("OpenAI Responses", ignored) + input_value = normalize_responses_input( + prompt, + system_prompt, + messages, + provider=getattr(self, "_provider", "openai"), + ) + request: dict[str, Any] = { + "model": self._model_name, + "input": input_value, + "stream": stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._RESPONSES_MODEL_OPTION_KEYS, + f"{getattr(self, '_provider', 'compatible')} Responses", + ) + # Keep the common model configuration ergonomic while emitting the + # current Responses SDK shape. These names are valid for Chat + # Completions, but Responses expects them under a different field. + configured_response_format = configured_options.pop("response_format", None) + configured_verbosity = configured_options.pop("verbosity", None) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + configured_max_output_tokens = configured_options.get("max_output_tokens") + configured_limits = { + key: value + for key, value in { + "max_output_tokens": configured_max_output_tokens, + "max_tokens": configured_max_tokens, + "max_completion_tokens": configured_max_completion_tokens, + }.items() + if value is not None + } + if len(configured_limits) > 1: + raise ValueError( + "Responses options 不能同时设置多个输出长度字段: " + + ", ".join(configured_limits) + ) + if configured_max_output_tokens is not None: + configured_options.pop("max_output_tokens", None) + configured_frequency_penalty = configured_options.pop("frequency_penalty", None) + configured_presence_penalty = configured_options.pop("presence_penalty", None) + configured_top_logprobs = configured_options.pop("top_logprobs", None) + configured_unsupported = { + key: configured_options.pop(key, None) + for key in ("thinking", "caching", "session", "expire_at") + } + reject_unsupported_kwargs("OpenAI Responses", configured_unsupported) + configured_background = (getattr(self, "_options", {}) or {}).get("background") + configured_stop = configured_options.pop("stop", None) + configured_seed = configured_options.pop("seed", None) + configured_reasoning_effort = configured_options.pop("reasoning_effort", None) + configured_text = configured_options.pop("text", None) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Responses", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_explicit_extra_keys = set(configured_extra_body) + self._validate_openai_field_compatibility(configured_options, "Responses") + if configured_text is not None: + if not isinstance(configured_text, Mapping): + raise ValueError("Responses options.text 必须是对象。") + request["text"] = dict(configured_text) + self._merge_options(request, configured_options) + configured_extra = self._compat_extra_options() + for key, value in (("stop", configured_stop), ("seed", configured_seed)): + if value is None: + continue + if key in configured_extra_body or key in configured_extra: + raise ValueError(f"Responses options.{key} 与 extra_body 重复。") + configured_extra[key] = value + merged_configured_extra = self._merge_extra_body( + configured_extra_body, configured_extra, "Responses" + ) + if merged_configured_extra: + request["extra_body"] = merged_configured_extra + if temperature is not None: + request["temperature"] = temperature + if system_prompt and messages is None: + request["instructions"] = system_prompt + converted_tools = self._convert_responses_tools(tools) + if converted_tools: + request["tools"] = converted_tools + if max_tokens is not None: + request["max_output_tokens"] = max_tokens + elif configured_limits: + request["max_output_tokens"] = next(iter(configured_limits.values())) + if top_p is not None: + request["top_p"] = top_p + effective_frequency_penalty = ( + frequency_penalty if frequency_penalty is not None else configured_frequency_penalty + ) + effective_presence_penalty = ( + presence_penalty if presence_penalty is not None else configured_presence_penalty + ) + unsupported_sampling = { + key: value + for key, value in { + "frequency_penalty": effective_frequency_penalty, + "presence_penalty": effective_presence_penalty, + }.items() + if value is not None + } + # OpenAI Responses 3.3 has no top-level stop/seed/frequency/presence + # fields. Keep these useful extensions for compatible gateways, but fail + # early for the official endpoint instead of sending a misleading request. + if self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", unsupported_sampling) + for key, value in unsupported_sampling.items(): + if key in configured_explicit_extra_keys: + raise ValueError(f"Responses {key} 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})[key] = value + # OpenAI Responses 3.3 has no top-level stop/seed/top_k fields. Keep + # these useful extensions for compatible gateways, but fail early for + # the official endpoint instead of sending a misleading request. + unsupported_official = { + key: value + for key, value in { + "stop": stop, + "seed": seed, + "top_k": top_k, + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + }.items() + if value is not None + } + configured_unsupported_official: dict[str, Any] = { + key: value + for key, value in { + **{ + key: self._options.get(key) + for key in ("stop", "seed", "top_k", "frequency_penalty", "presence_penalty") + if key in getattr(self, "_options", {}) + }, + **{ + key: merged_configured_extra.get(key) + for key in ( + "stop", + "seed", + "top_k", + "frequency_penalty", + "presence_penalty", + ) + if key in merged_configured_extra + }, + }.items() + if value is not None + } + if configured_unsupported_official and self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", configured_unsupported_official) + if unsupported_official and self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", unsupported_official) + if top_k is not None: + if "top_k" in configured_explicit_extra_keys: + raise ValueError("Responses top_k 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_explicit_extra_keys: + raise ValueError("Responses seed 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["seed"] = seed + if stop is not None: + if "stop" in configured_explicit_extra_keys: + raise ValueError("Responses stop 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["stop"] = stop + effective_response_format = ( + response_format if response_format is not None else configured_response_format + ) + if effective_response_format is not None: + request.setdefault("text", {}).update( + self._convert_responses_format(effective_response_format) + ) + response_extensions = { + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "safety_identifier": safety_identifier, + "moderation": moderation, + "verbosity": verbosity, + "store": store, + } + self._validate_openai_field_compatibility( + response_extensions, "Responses", protocol="responses" + ) + if tool_choice is not None: + request["tool_choice"] = self._convert_responses_tool_choice(tool_choice) + elif "tool_choice" in request: + request["tool_choice"] = self._convert_responses_tool_choice(request["tool_choice"]) + if metadata is not None: + request["metadata"] = metadata + if user is not None: + request["user"] = user + if timeout is not None: + request["timeout"] = timeout + if extra_headers is not None: + request["extra_headers"] = extra_headers + if extra_query is not None: + request["extra_query"] = extra_query + if previous_response_id is not None: + request["previous_response_id"] = previous_response_id + if include is not None: + request["include"] = include + if reasoning is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError("Responses reasoning 与模型 options.extra_body 重复。") + request["reasoning"] = reasoning + elif configured_reasoning_effort is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Responses reasoning_effort 与模型 options.extra_body 重复。" + ) + request["reasoning"] = {"effort": configured_reasoning_effort} + if store is not None: + request["store"] = store + if background is not None: + request["background"] = background + if parallel_tool_calls is not None: + request["parallel_tool_calls"] = parallel_tool_calls + if max_tool_calls is not None: + request["max_tool_calls"] = max_tool_calls + if conversation is not None: + request["conversation"] = conversation + if context_management is not None: + request["context_management"] = context_management + if prompt_cache_options is not None: + request["prompt_cache_options"] = prompt_cache_options + if safety_identifier is not None: + request["safety_identifier"] = safety_identifier + if moderation is not None: + request["moderation"] = moderation + effective_top_logprobs = ( + top_logprobs if top_logprobs is not None else configured_top_logprobs + ) + if effective_top_logprobs is not None: + request["top_logprobs"] = effective_top_logprobs + effective_verbosity = verbosity if verbosity is not None else configured_verbosity + if effective_verbosity is not None: + # openai>=3 accepts Responses verbosity inside `text`; it is not + # a valid top-level argument for `responses.create()`. + request.setdefault("text", {})["verbosity"] = effective_verbosity + if prompt_cache_key is not None: + request["prompt_cache_key"] = prompt_cache_key + if prompt_cache_retention is not None: + request["prompt_cache_retention"] = prompt_cache_retention + if service_tier is not None: + request["service_tier"] = service_tier + if truncation is not None: + request["truncation"] = truncation + if stream_options is not None: + request["stream_options"] = stream_options + effective_background = background if background is not None else configured_background + if effective_background is not None: + if not isinstance(effective_background, bool): + raise ValueError("Responses background 必须是布尔值。") + if effective_background and stream: + raise ValueError("Responses background 请求不支持流式模式。") + request["background"] = effective_background + request_extra_body = self._validated_extra_body( + extra_body, + "Responses", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Responses extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Responses", + set(request).difference({"extra_body"}), + ) + return request + + @staticmethod + def _convert_responses_format(response_format: dict[str, Any]) -> dict[str, Any]: + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + json_schema = schema.get("schema", {}) if isinstance(schema, Mapping) else {} + return { + "format": { + "type": "json_schema", + "name": schema.get("name", "response") if isinstance(schema, Mapping) else "response", + "schema": json_schema or response_format.get("schema", schema), + "strict": schema.get("strict", True) if isinstance(schema, Mapping) else True, + } + } + if response_format.get("type") == "json_object": + return {"format": {"type": "json_object"}} + if "format" in response_format: + return response_format + raise ValueError("Responses response_format 仅支持 json_schema 或 json_object。") + + @staticmethod + def _field(value: Any, name: str, default: Any = None) -> Any: + if isinstance(value, Mapping): + return value.get(name, default) + return getattr(value, name, default) + + @classmethod + def _extract_response_text(cls, response: Any) -> str: + output_text = cls._field(response, "output_text") + if isinstance(output_text, str): + return output_text + + chunks: list[str] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + text = cls._field(message, "content") + if text: + chunks.append(content_to_text(text)) + if chunks: + return "".join(chunks) + for item in cls._field(response, "output", []) or []: + for content in cls._field(item, "content", []) or []: + text = cls._field(content, "text") + if isinstance(text, str): + chunks.append(text) + return "".join(chunks) + + @classmethod + def _extract_tool_calls(cls, response: Any) -> list[dict[str, Any]]: + calls: list[dict[str, Any]] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + for call in cls._field(message, "tool_calls", []) or []: + function = cls._field(call, "function") + calls.append( + { + "id": cls._field(call, "id"), + "type": cls._field(call, "type", "function"), + "name": cls._field(function, "name"), + "arguments": normalize_tool_arguments(cls._field(function, "arguments", "")), + } + ) + for item in cls._field(response, "output", []) or []: + if cls._field(item, "type") in {"function_call", "custom_tool_call"}: + calls.append( + { + "id": cls._field(item, "call_id") or cls._field(item, "id"), + "type": cls._field(item, "type"), + "name": cls._field(item, "name"), + "arguments": normalize_tool_arguments( + cls._field(item, "arguments", cls._field(item, "input", "")) + ), + } + ) + return calls + + @classmethod + def _extract_usage(cls, response: Any) -> dict[str, Any]: + return normalize_usage(cls._field(response, "usage")) + + @classmethod + def _extract_reasoning(cls, response: Any) -> str: + values: list[str] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + for name in ("reasoning_content", "reasoning"): + value = cls._field(message, name) + if isinstance(value, str): + values.append(value) + for item in cls._field(response, "output", []) or []: + if cls._field(item, "type") in {"reasoning", "reasoning_item"}: + summary = cls._field(item, "summary") + if isinstance(summary, str): + values.append(summary) + elif isinstance(summary, (list, tuple)): + for entry in summary: + text = cls._field(entry, "text") + if isinstance(text, str): + values.append(text) + for content in cls._field(item, "content", []) or []: + text = cls._field(content, "text") + if isinstance(text, str): + values.append(text) + fallback = cls._field(item, "text") + if isinstance(fallback, str): + values.append(fallback) + return "".join(values) + + @classmethod + def _extract_refusal(cls, response: Any) -> str | None: + """提取 Chat Completions 与 Responses 的统一拒答文本。""" + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + refusal = cls._field(message, "refusal") + if isinstance(refusal, str) and refusal: + return refusal + for item in cls._field(response, "output", []) or []: + refusal = cls._field(item, "refusal") + if isinstance(refusal, str) and refusal: + return refusal + for content in cls._field(item, "content", []) or []: + if cls._field(content, "type") != "refusal": + continue + refusal = cls._field(content, "refusal") or cls._field(content, "text") + if isinstance(refusal, str) and refusal: + return refusal + return None + + @classmethod + def _chat_status_is_error(cls, status: Any) -> bool: + """识别兼容网关放在 HTTP 200 响应体中的业务失败状态。""" + if status is None or isinstance(status, bool): + return False + if isinstance(status, int): + return status >= 400 + normalized = str(status).strip().lower() + if not normalized: + return False + if normalized.isdigit(): + return int(normalized) >= 400 + return normalized in { + "error", + "failed", + "failure", + "cancelled", + "canceled", + "incomplete", + "rejected", + "denied", + } or normalized.startswith(("error_", "failed_", "failure_")) + + @classmethod + def _chat_response_error_detail(cls, response: Any) -> str | None: + """提取兼容网关常见的顶层业务错误字段。""" + for name in ("error", "msg", "message", "detail", "reason", "code", "error_code"): + value = cls._field(response, name) + if value is None or value == "": + continue + if name == "error": + return redact_sensitive_text(cls._error_message(value, "未知错误。")) + return redact_sensitive_text(str(value)) + return None + + @classmethod + def _raise_for_chat_response_error( + cls, + response: Any, + *, + allow_empty_choices: bool = False, + provider: str = "Chat Completions", + ) -> None: + """把 Chat Completions 的协议错误和网关业务错误转换为异常。 + + 部分兼容网关会以 HTTP 200 返回 status/msg,并将 choices 设为 + None。如果只按 SDK 的异常或 choices 提取,这类失败会被误报为空文本。 + 流式响应允许没有 choices 的 usage chunk,但只要出现错误状态或错误 + 字段仍必须显式失败。 + """ + detail = cls._chat_response_error_detail(response) + status = cls._field(response, "status") + if detail is not None and cls._field(response, "error") is not None: + raise RuntimeError(f"{provider} 返回错误: {detail}") + if cls._chat_status_is_error(status): + suffix = f": {detail}" if detail else "。" + raise RuntimeError(f"{provider} 返回业务错误 (status={status}){suffix}") + + choices = cls._field(response, "choices") + if allow_empty_choices: + if choices is None and detail is not None: + raise RuntimeError(f"{provider} 返回错误: {detail}") + return + if choices is None or (isinstance(choices, (list, tuple)) and not choices): + suffix = f": {detail}" if detail else "。" + raise RuntimeError(f"{provider} 响应缺少有效 choices{suffix}") + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible complete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("complete") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response = retry_sync_call( + lambda: self._get_client().responses.create(**params) + ) + if params.get("background"): + response = self._poll_background_response(response, params) + self._raise_for_response_error(response) + else: + params = self._build_chat_request(**request.to_invoke_kwargs()) + response = retry_sync_call( + lambda: self._get_client().chat.completions.create(**params) + ) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + choices = self._field(response, "choices", []) or [] + finish_reason = ( + self._field(response, "status") + if self._protocol == "responses" + else self._field(choices[0], "finish_reason") if choices else None + ) + return CompletionResult( + text=self._extract_response_text(response), + tool_calls=self._extract_tool_calls(response), + usage=self._extract_usage(response), + finish_reason=finish_reason, + response_id=self._field(response, "id"), + refusal=self._extract_refusal(response), + reasoning=self._extract_reasoning(response), + raw=response, + ) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncOpenAI 聚合完整结果,保留 SDK 元数据。""" + request = coerce_completion_request(request, kwargs, "OpenAI-compatible acomplete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("acomplete") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response = await retry_async_call( + lambda: self._get_aclient().responses.create(**params) + ) + if params.get("background"): + response = await self._poll_background_response_async(response, params) + self._raise_for_response_error(response) + else: + params = self._build_chat_request(**request.to_invoke_kwargs()) + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create(**params) + ) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + choices = self._field(response, "choices", []) or [] + finish_reason = ( + self._field(response, "status") + if self._protocol == "responses" + else self._field(choices[0], "finish_reason") if choices else None + ) + return CompletionResult( + text=self._extract_response_text(response), + tool_calls=self._extract_tool_calls(response), + usage=self._extract_usage(response), + finish_reason=finish_reason, + response_id=self._field(response, "id"), + refusal=self._extract_refusal(response), + reasoning=self._extract_reasoning(response), + raw=response, + ) + + @classmethod + def _chat_stream_events(cls, chunk: Any) -> list[StreamEvent]: + raise_for_stream_error_event(chunk, "Chat Completions") + cls._raise_for_chat_response_error(chunk, allow_empty_choices=True) + choices = cls._field(chunk, "choices", []) or [] + usage = cls._extract_usage(chunk) + if not choices: + return [StreamEvent(type="usage", usage=usage, raw=chunk)] if usage else [] + choice = choices[0] + delta = cls._field(choice, "delta") + text = content_to_text(cls._field(delta, "content", "")) + reasoning = cls._field(delta, "reasoning_content") or cls._field(delta, "reasoning") or "" + refusal = cls._field(delta, "refusal") + tool_calls = cls._field(delta, "tool_calls", []) or [] + finish_reason = cls._field(choice, "finish_reason") + events: list[StreamEvent] = [] + + if isinstance(refusal, str) and refusal: + events.append(StreamEvent(type="refusal_delta", refusal=refusal, raw=chunk)) + + response_id = cls._field(chunk, "id") + if text: + events.append( + StreamEvent( + type="text_delta", + text=text, + response_id=response_id, + raw=chunk, + ) + ) + if isinstance(reasoning, str) and reasoning: + events.append( + StreamEvent( + type="reasoning_delta", + reasoning=reasoning, + response_id=response_id, + raw=chunk, + ) + ) + if tool_calls: + for position, call in enumerate(tool_calls): + function = cls._field(call, "function") + events.append( + StreamEvent( + type="tool_call_delta", + tool_call={ + "id": cls._field(call, "id"), + "index": cls._field(call, "index", position), + "type": cls._field(call, "type", "function"), + "name": cls._field(function, "name"), + "arguments": cls._field(function, "arguments", ""), + }, + response_id=response_id, + raw=chunk, + ) + ) + + if finish_reason: + events.append( + StreamEvent( + type="finish", + usage=usage, + finish_reason=finish_reason, + response_id=cls._field(chunk, "id"), + raw=chunk, + ) + ) + elif usage: + if events: + events[-1].usage = usage + else: + events.append(StreamEvent(type="usage", usage=usage, raw=chunk)) + return events + + @classmethod + def _chat_stream_event(cls, chunk: Any) -> StreamEvent | None: + """兼容旧调用方,返回当前 chunk 的第一个统一事件。""" + events = cls._chat_stream_events(chunk) + return events[0] if events else None + + @classmethod + def _response_tool_keys(cls, event: Any, item: Any = None) -> list[Any]: + """返回 Responses 工具事件可用的稳定关联键。""" + item = item if item is not None else event + item_id = cls._field(event, "item_id") + if item_id is None: + item_id = cls._field(item, "id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = cls._field(item, "output_index") + call_id = cls._field(event, "call_id") + if call_id is None: + call_id = cls._field(item, "call_id") + keys: list[Any] = [] + for prefix, value in ( + ("item_id", item_id), + ("index", output_index), + ("call_id", call_id), + ): + if value is None: + continue + try: + hash(value) + except TypeError: + continue + keys.append((prefix, value)) + return keys + + @classmethod + def _remember_response_tool_metadata( + cls, + event: Any, + metadata: dict[Any, dict[str, Any]], + ) -> None: + """记录 output_item.added 中后续 delta/done 事件需要的工具元数据。""" + item = cls._field(event, "item") or event + item_type = cls._field(item, "type") + if item_type not in {"function_call", "custom_tool_call"}: + return + item_id = cls._field(item, "id") or cls._field(event, "item_id") + call_id = cls._field(item, "call_id") or cls._field(event, "call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = cls._field(item, "output_index") + values: dict[str, Any] = { + "id": call_id or item_id, + "call_id": call_id, + "index": output_index, + "type": item_type, + "name": cls._field(item, "name"), + } + for field_name in ("arguments", "input"): + value = cls._field(item, field_name) + if value is not None: + values["arguments"] = normalize_tool_arguments(value) + break + for key in cls._response_tool_keys(event, item): + stored = metadata.setdefault(key, {}) + for name, value in values.items(): + if value is not None and value != "": + stored[name] = value + + @classmethod + def _response_tool_metadata( + cls, + event: Any, + metadata: Mapping[Any, Mapping[str, Any]] | None, + item: Any = None, + ) -> dict[str, Any]: + """按 item_id/output_index/call_id 合并已记录的工具元数据。""" + if not metadata: + return {} + merged: dict[str, Any] = {} + for key in cls._response_tool_keys(event, item): + values = metadata.get(key) + if not values: + continue + for name, value in values.items(): + merged.setdefault(name, value) + return merged + + @classmethod + def _responses_stream_event( + cls, + event: Any, + tool_metadata: dict[Any, dict[str, Any]] | None = None, + *, + error_provider: str = "Responses API", + response_error_handler: Any | None = None, + ) -> StreamEvent | None: + event_type = cls._field(event, "type", "") + if event_type == "response.output_item.added": + if tool_metadata is not None: + cls._remember_response_tool_metadata(event, tool_metadata) + return None + if event_type in {"response.output_text.delta", "response.refusal.delta"}: + delta = cls._field(event, "delta") + is_refusal = event_type.endswith("refusal.delta") + return StreamEvent( + type="refusal_delta" if is_refusal else "text_delta", + text="" if is_refusal else (delta if isinstance(delta, str) else ""), + refusal=delta if is_refusal and isinstance(delta, str) else None, + raw=event, + ) + if event_type in {"response.reasoning_summary_text.delta", "response.reasoning_text.delta"}: + delta = cls._field(event, "delta") + return StreamEvent(type="reasoning_delta", reasoning=delta if isinstance(delta, str) else "", raw=event) + if event_type.endswith(".delta") and ( + "function_call_arguments" in event_type or "custom_tool_call_input" in event_type + ): + metadata = cls._response_tool_metadata(event, tool_metadata) + item_type = metadata.get( + "type", + "custom_tool_call" if "custom_tool_call_input" in event_type else "function_call", + ) + item_id = cls._field(event, "item_id") + call_id = cls._field(event, "call_id") or metadata.get("call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = metadata.get("index") + return StreamEvent( + type="tool_call_delta", + tool_call={ + "id": item_id or cls._field(event, "call_id") or metadata.get("id"), + "call_id": call_id, + "index": output_index if output_index is not None else cls._field(event, "index"), + "type": item_type, + "name": cls._field(event, "name") or metadata.get("name"), + "arguments": normalize_tool_arguments(cls._field(event, "delta", "")), + }, + raw=event, + ) + if event_type in { + "response.output_item.done", + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + item = cls._field(event, "item") + metadata = cls._response_tool_metadata(event, tool_metadata, item) + item_type = cls._field(item, "type") if item is not None else None + item_type = item_type or metadata.get("type") + if ( + item_type in {"function_call", "custom_tool_call"} + or "function_call_arguments" in event_type + or "custom_tool_call_input" in event_type + ): + call_id = cls._field(item, "call_id") or cls._field(event, "call_id") or metadata.get("call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = metadata.get("index") + argument_value: Any = None + for source in (item, event): + for field_name in ("arguments", "input"): + value = cls._field(source, field_name) + if value is not None: + argument_value = value + break + if argument_value is not None: + break + if argument_value is None: + argument_value = metadata.get("arguments", "") + tool_call: dict[str, Any] = { + "id": cls._field(item, "call_id") + or cls._field(item, "id") + or cls._field(event, "item_id") + or metadata.get("id"), + "index": output_index if output_index is not None else cls._field(event, "index"), + "type": item_type if item_type in {"function_call", "custom_tool_call"} else "function_call", + "name": cls._field(item, "name") or cls._field(event, "name") or metadata.get("name"), + "arguments": normalize_tool_arguments(argument_value), + } + if call_id is not None: + tool_call["call_id"] = call_id + return StreamEvent( + type="tool_call_completed", + tool_call=tool_call, + raw=event, + ) + if event_type in {"error", "response.error"}: + raise_for_stream_error_event(event, error_provider) + if event_type in { + "response.completed", "response.incomplete", "response.failed", "response.cancelled" + }: + response = cls._field(event, "response") or event + usage = cls._extract_usage(response) + status = cls._field(response, "status") + if status in {"failed", "incomplete", "cancelled"}: + handler = response_error_handler or cls._raise_for_response_error + handler(response) + if event_type != "response.completed": + raise RuntimeError(f"{error_provider} 收到终止事件: {event_type}。") + return StreamEvent( + type="finish", + usage=usage, + finish_reason=status, + response_id=cls._field(response, "id"), + raw=event, + ) + return None + + @classmethod + def _merge_response_tool_call( + cls, + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + ) -> tuple[Any, dict[str, Any]]: + """合并 Responses 的完整工具调用元数据。 + + ``function_call_arguments.done`` 使用 item_id,而 + ``output_item.done`` 通常带 call_id;两者必须按 output_index 归并。 + """ + index = tool_call.get("index") + call_id = tool_call.get("call_id") or tool_call.get("id") + key: Any = ("index", index) if index is not None else ("id", call_id) + if key == ("id", None): + key = ("position", len(accumulator)) + merged = accumulator.setdefault(key, {}) + for name in ("id", "call_id", "index", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + arguments = tool_call.get("arguments") + if arguments is not None and arguments != "": + merged["arguments"] = normalize_tool_arguments(arguments) + merged.setdefault("type", "function_call") + merged.setdefault("arguments", "") + return key, merged + + @classmethod + def _chat_completion_events( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + completed: set[Any], + ) -> list[StreamEvent]: + """在 Chat Completions finish 前发出一次完整工具调用事件。""" + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(accumulator, converted.tool_call) + return [converted] + if converted.type != "finish": + return [converted] + + events: list[StreamEvent] = [] + for key, tool_call in accumulator.items(): + if key in completed: + continue + validate_complete_tool_call(tool_call, "OpenAI Chat Completions") + completed.add(key) + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + usage={}, + finish_reason=converted.finish_reason, + response_id=converted.response_id, + raw=converted.raw, + ) + ) + events.append(converted) + return events + + @classmethod + def _responses_completion_event( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + ) -> tuple[Any, StreamEvent] | None: + if converted.type != "tool_call_completed" or not converted.tool_call: + return None + key, merged = cls._merge_response_tool_call(accumulator, converted.tool_call) + validate_complete_tool_call(merged, "OpenAI Responses") + return key, StreamEvent( + type="tool_call_completed", + tool_call=dict(merged), + response_id=converted.response_id, + raw=converted.raw, + ) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible stream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("stream_events") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create(**params), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + if terminal_seen: + continue + event_type = self._field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "OpenAI Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep a response-body generator open + # after the terminal event; breaking here can surface + # noisy generator-close errors and leak a connection. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Responses API 流在 response.completed 之前结束。") + return + params = self._build_chat_request(**request.to_invoke_kwargs()) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create(**params), + lambda chunk: raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ), + ): + raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ) + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + yield from self._chat_completion_events(converted, tool_calls, completed_chat_tool_calls) + if not terminal_seen: + raise RuntimeError("Chat Completions 流在 finish 事件之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible astream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("astream_events") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create(**params), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + if terminal_seen: + continue + event_type = self._field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "OpenAI Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep the response body generator + # open after the terminal event. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Responses API 异步流在 response.completed 之前结束。") + return + params = self._build_chat_request(**request.to_invoke_kwargs()) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create(**params), + lambda chunk: raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ), + ): + raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ) + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + for event in self._chat_completion_events(converted, tool_calls, completed_chat_tool_calls): + yield event + if not terminal_seen: + raise RuntimeError("Chat Completions 异步流在 finish 事件之前结束。") + + def parse( + self, + text_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """调用 OpenAI Chat/Responses 的原生结构化解析入口。""" + if text_format is None: + raise ValueError("OpenAI parse 必须提供 text_format。") + request = coerce_completion_request(request, kwargs, "OpenAI-compatible parse") + request = request.copy_with(stream=False) + values = request.to_invoke_kwargs() + if self._protocol == "responses": + self._require_responses_resource("parse") + params = self._build_responses_request(**values) + if params.get("background"): + raise ValueError( + "Responses parse 不支持 background=true;" + "请使用 complete/acomplete 等待后台任务完成后再处理结果。" + ) + # ``stream`` is an internal unified-request control flag. The + # Responses parse helper always returns one parsed result and its + # SDK signature does not accept this flag. + params.pop("stream", None) + text_config = params.pop("text", None) + if isinstance(text_config, Mapping) and "format" in text_config: + raise ValueError( + "Responses parse 不能同时使用 request.response_format 和 text_format。" + ) + if text_config: + params["text"] = text_config + return self._get_client().responses.parse(text_format=text_format, **params) + params = self._build_chat_request(**values) + params.pop("response_format", None) + params.pop("stream", None) + return self._get_client().chat.completions.parse(response_format=text_format, **params) + + async def async_parse( + self, + text_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 OpenAI Chat/Responses 的原生结构化解析入口。""" + if text_format is None: + raise ValueError("OpenAI parse 必须提供 text_format。") + request = coerce_completion_request(request, kwargs, "OpenAI-compatible async_parse") + request = request.copy_with(stream=False) + values = request.to_invoke_kwargs() + if self._protocol == "responses": + self._require_responses_resource("parse") + params = self._build_responses_request(**values) + if params.get("background"): + raise ValueError( + "Responses async_parse 不支持 background=true;" + "请使用 acomplete 等待后台任务完成后再处理结果。" + ) + params.pop("stream", None) + text_config = params.pop("text", None) + if isinstance(text_config, Mapping) and "format" in text_config: + raise ValueError( + "Responses parse 不能同时使用 request.response_format 和 text_format。" + ) + if text_config: + params["text"] = text_config + return await self._resolve_async_result( + self._get_aclient().responses.parse(text_format=text_format, **params) + ) + params = self._build_chat_request(**values) + params.pop("response_format", None) + params.pop("stream", None) + return await self._resolve_async_result( + self._get_aclient().chat.completions.parse(response_format=text_format, **params) + ) + + @classmethod + def _error_message(cls, error: Any, default: str) -> str: + if isinstance(error, str) and error: + return error + message = cls._field(error, "message") + if message: + return str(message) + code = cls._field(error, "code") + if code: + return f"{code}" + return default + + @classmethod + def _raise_for_response_error(cls, response: Any) -> None: + error = cls._field(response, "error") + if error: + raise RuntimeError( + f"Responses API 返回错误: {cls._error_message(error, '未知错误。')}" + ) + + status = cls._field(response, "status") + if status not in {"failed", "incomplete", "cancelled"}: + return + + if status == "incomplete": + details = cls._field(response, "incomplete_details") + reason = cls._field(details, "reason") + detail = f"原因: {reason}" if reason else "未提供原因。" + else: + detail = "未提供原因。" + raise RuntimeError(f"Responses API 响应状态为 {status}: {detail}") + + @classmethod + def _stream_delta(cls, event: Any) -> str: + event_type = cls._field(event, "type", "") + if event_type in {"response.output_text.delta", "response.refusal.delta"}: + delta = cls._field(event, "delta") + return delta if isinstance(delta, str) else "" + if event_type in {"error", "response.error"}: + raise_for_stream_error_event(event, "Responses API") + if event_type in { + "response.completed", "response.failed", "response.incomplete", "response.cancelled" + }: + response = cls._field(event, "response") or event + cls._raise_for_response_error(response) + if event_type != "response.completed": + raise RuntimeError(f"Responses API 收到终止事件: {event_type}。") + return "" + + def _invoke_responses(self, request: dict[str, Any]) -> Iterator[str]: + if request["stream"]: + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create(**request), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + event_type = self._field(event, "type", "") + if event_type == "response.refusal.delta": + refusal = self._field(event, "delta") + if refusal: + raise RuntimeError( + f"{self._provider} Responses 返回拒答: {refusal}" + ) + delta = self._stream_delta(event) + if delta: + yield delta + if event_type == "response.completed": + terminal_seen = True + if not terminal_seen: + raise RuntimeError("Responses API 流在 response.completed 之前结束。") + return + response = retry_sync_call(lambda: self._get_client().responses.create(**request)) + if request.get("background"): + response = self._poll_background_response(response, request) + self._raise_for_response_error(response) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Responses 返回拒答: {refusal}") + text = self._extract_response_text(response) + if text: + yield text + + async def _ainvoke_responses(self, request: dict[str, Any]) -> AsyncIterator[str]: + if request["stream"]: + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create(**request), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + event_type = self._field(event, "type", "") + if event_type == "response.refusal.delta": + refusal = self._field(event, "delta") + if refusal: + raise RuntimeError( + f"{self._provider} Responses 返回拒答: {refusal}" + ) + delta = self._stream_delta(event) + if delta: + yield delta + if event_type == "response.completed": + terminal_seen = True + if not terminal_seen: + raise RuntimeError("Responses API 异步流在 response.completed 之前结束。") + return + response = await retry_async_call(lambda: self._get_aclient().responses.create(**request)) + if request.get("background"): + response = await self._poll_background_response_async(response, request) + self._raise_for_response_error(response) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Responses 返回拒答: {refusal}") + text = self._extract_response_text(response) + if text: + yield text + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> Generator[str, None, None]: """同步调用 OpenAI 兼容 LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"调用 {self._provider} LLM ({self._model_name}),流式: {stream}") + if self._protocol == "responses": + self._require_responses_resource("invoke") + if tools: + raise ValueError( + f"{self._provider} Responses invoke 仅返回文本,不支持 tools;" + "请使用 complete 或 stream_events 获取工具调用。" + ) client = self._get_client() start_time = time.perf_counter() try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream + if self._protocol == "responses": + yield from self._invoke_responses( + self._build_responses_request(prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, + top_k=top_k, seed=seed, + stop=stop, response_format=response_format, tool_choice=tool_choice, + extra_body=extra_body, metadata=metadata, user=user, timeout=timeout, + **kwargs) + ) + return + + request = self._build_chat_request( + prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, stop=stop, + top_k=top_k, seed=seed, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, metadata=metadata, + user=user, timeout=timeout, **kwargs, ) - if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + for chunk in retry_sync_stream( + lambda: client.chat.completions.create(**request), + lambda chunk: raise_for_stream_error_event(chunk, self._provider), + ): + raise_for_stream_error_event(chunk, self._provider) + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError( + f"{self._provider} Chat Completions 返回拒答: {event.refusal}" + ) + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Chat Completions 流在 finish 事件之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content + response = retry_sync_call(lambda: client.chat.completions.create(**request)) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Chat Completions 返回拒答: {refusal}") + choices = self._field(response, "choices", []) or [] + if choices: + message = self._field(choices[0], "message") + content = self._field(message, "content") + if content: + yield content_to_text(content) duration = time.perf_counter() - start_time logger.info(f"{self._provider} LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"{self._provider} LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s LLM (%s) 出错: %s", + self._provider, + self._model_name, + error_text, + ) raise async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> AsyncGenerator[str, None]: """异步调用 OpenAI 兼容 LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"异步调用 {self._provider} LLM ({self._model_name}),流式: {stream}") + if self._protocol == "responses": + self._require_responses_resource("ainvoke") + if tools: + raise ValueError( + f"{self._provider} Responses ainvoke 仅返回文本,不支持 tools;" + "请使用 acomplete 或 astream_events 获取工具调用。" + ) aclient = self._get_aclient() start_time = time.perf_counter() try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream + if self._protocol == "responses": + async for chunk in self._ainvoke_responses( + self._build_responses_request(prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, + top_k=top_k, seed=seed, + stop=stop, response_format=response_format, tool_choice=tool_choice, + extra_body=extra_body, metadata=metadata, user=user, timeout=timeout, + **kwargs) + ): + yield chunk + return + + request = self._build_chat_request( + prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, stop=stop, + top_k=top_k, seed=seed, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, metadata=metadata, + user=user, timeout=timeout, **kwargs, ) - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + async for chunk in retry_async_stream( + lambda: aclient.chat.completions.create(**request), + lambda chunk: raise_for_stream_error_event(chunk, self._provider), + ): + raise_for_stream_error_event(chunk, self._provider) + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError( + f"{self._provider} Chat Completions 返回拒答: {event.refusal}" + ) + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Chat Completions 异步流在 finish 事件之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content + response = await retry_async_call(lambda: aclient.chat.completions.create(**request)) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Chat Completions 返回拒答: {refusal}") + choices = self._field(response, "choices", []) or [] + if choices: + message = self._field(choices[0], "message") + content = self._field(message, "content") + if content: + yield content_to_text(content) duration = time.perf_counter() - start_time logger.info(f"{self._provider} LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"{self._provider} LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s LLM (%s) 异步出错: %s", + self._provider, + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步向量化文档。""" logger.info(f"调用 {self._provider} 嵌入 ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"{self._provider} Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + self._validate_embedding_kwargs(kwargs) + client = self._get_client() try: - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options).union(request_options), + ) + option_overlap = sorted(set(configured_options).intersection(request_options)) + if option_overlap: + raise ValueError( + f"{self._provider} Embedding 请求参数与模型 options 重复: " + + ", ".join(option_overlap) + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, f"{self._provider} Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = client.embeddings.create(**request) + embeddings = [normalize_embedding_vector(item.embedding) for item in response.data] duration = time.perf_counter() - start_time logger.info(f"{self._provider} 嵌入 ({self._model_name}) 完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"{self._provider} 嵌入 ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s 嵌入 (%s) 出错: %s", + self._provider, + self._model_name, + error_text, + ) raise - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + @retry( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True + ) + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步向量化文档。""" logger.info(f"异步调用 {self._provider} 嵌入 ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() start_time = time.perf_counter() + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"{self._provider} Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + self._validate_embedding_kwargs(kwargs) + aclient = self._get_aclient() try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options).union(request_options), + ) + option_overlap = sorted(set(configured_options).intersection(request_options)) + if option_overlap: + raise ValueError( + f"{self._provider} Embedding 请求参数与模型 options 重复: " + + ", ".join(option_overlap) + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, f"{self._provider} Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = await self._resolve_async_result(aclient.embeddings.create(**request)) + embeddings = [normalize_embedding_vector(item.embedding) for item in response.data] duration = time.perf_counter() - start_time logger.info(f"{self._provider} 嵌入 ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"{self._provider} 嵌入 ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s 嵌入 (%s) 异步出错: %s", + self._provider, + self._model_name, + error_text, + ) raise + + def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + method = self._get_client().files.create + return self._call_sdk_resource( + method, + kwargs=self._merge_resource_kwargs( + {"file": file, "purpose": cast(Any, purpose)}, + kwargs, + "OpenAI 文件上传", + ), + operation="OpenAI 文件上传", + ) + + def list_files(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.list, kwargs=kwargs, operation="OpenAI 文件列表" + ) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.retrieve, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件读取", + ) + + def file_content(self, file_id: str, **kwargs: Any) -> Any: + # openai>=3 exposes binary/text file content as `files.content`. + files = getattr(self._get_client(), "files", None) + method = getattr(files, "content", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供文件内容读取资源(files.content)。" + ) + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件内容读取", + ) + + def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + """读取文件文本内容;兼容 SDK 的 retrieve_content 命名。""" + method = getattr(self._get_client().files, "retrieve_content", None) + if method is not None: + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件文本读取", + ) + return self.file_content(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.delete, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件删除", + ) + + def wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """等待文件处理完成,仅转发 OpenAI SDK 的轮询参数。""" + reject_unsupported_kwargs("OpenAI 文件等待", kwargs) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().files.wait_for_processing, + kwargs=self._merge_resource_kwargs( + {"id": file_id}, poll_kwargs, "OpenAI 文件等待" + ), + operation="OpenAI 文件等待", + ) + + def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + if self._protocol == "responses" and ( + endpoint is None or endpoint.rstrip("/") == "/v1/responses" + ): + self._require_responses_resource("create_batch") + if endpoint is None: + endpoint = "/v1/responses" if self._protocol == "responses" else "/v1/chat/completions" + kwargs.setdefault("completion_window", "24h") + return self._call_sdk_resource( + self._get_client().batches.create, + kwargs=self._merge_resource_kwargs( + { + "input_file_id": input_file_id, + "endpoint": cast(Any, endpoint), + }, + kwargs, + "OpenAI Batch 创建", + ), + operation="OpenAI Batch 创建", + ) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.retrieve, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 读取", + ) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.cancel, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 取消", + ) + + def list_batches(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.list, kwargs=kwargs, operation="OpenAI Batch 列表" + ) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + return self._call_sdk_resource( + self._get_client().responses.retrieve, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 读取", + ) + + def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("cancel_response") + return self._call_sdk_resource( + self._get_client().responses.cancel, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 取消", + ) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + return self._call_sdk_resource( + self._get_client().responses.delete, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 删除", + ) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + return self._call_sdk_resource( + self._get_client().responses.input_items.list, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 输入项列表", + ) + + def connect_responses(self, **kwargs: Any) -> Any: + """打开官方 Responses WebSocket 连接管理器。""" + self._require_responses_resource("connect_responses") + return self._call_sdk_resource( + self._get_client().responses.connect, + kwargs=kwargs, + operation="OpenAI Responses 连接", + ) + + def _prepare_responses_stream_params(self, params: dict[str, Any]) -> dict[str, Any]: + """将统一参数收敛到 ``responses.stream`` 的 SDK 契约。""" + # The SDK uses response_id/starting_after to continue an existing + # response and rejects model in that mode. A native caller may pass + # either value explicitly, so remove the provider default as well as + # an explicitly supplied model before dispatch. + if "response_id" in params or "starting_after" in params: + params.pop("model", None) + else: + params.setdefault("model", self._model_name) + params.pop("stream", None) + if "verbosity" in params: + verbosity = params.pop("verbosity") + if verbosity is not None: + params.setdefault("text", {}).setdefault("verbosity", verbosity) + return params + + def stream_responses(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回官方 Responses 流上下文管理器,支持统一请求或原生参数。""" + self._require_responses_resource("stream_responses") + if request is not None and kwargs: + raise ValueError("stream_responses 不能同时传 request 和原生关键字参数。") + if request is not None: + params = self._build_responses_request(**request.copy_with(stream=True).to_invoke_kwargs()) + params.pop("stream", None) + else: + params = dict(kwargs) + # `responses.stream` is a helper with a narrower contract than + # `responses.create`: it does not accept the create-only `stream` + # switch, and it uses `starting_after` for continuation. Keep the + # provider's unified request shape while validating the helper boundary. + params = self._prepare_responses_stream_params(params) + return self._call_sdk_resource( + self._get_client().responses.stream, + kwargs=params, + operation="OpenAI Responses 流", + ) + + def _build_token_count_request(self, request: CompletionRequest) -> dict[str, Any]: + validate_secret_free_request_overrides( + request.extra_headers, + request.extra_query, + f"{getattr(self, '_provider', 'openai')} Responses input token count", + ) + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in { + "prompt", "system_prompt", "messages", "tools", "tool_choice", + "parallel_tool_calls", "reasoning", "previous_response_id", + "personality", "response_format", "truncation", "conversation", + "extra_headers", "extra_query", "extra_body", "timeout", + # Shared generation defaults are not part of the token-count + # request, but accepting and dropping them is necessary for + # CompletionRequest compatibility. They are never forwarded. + "stream", "temperature", + } + } + reject_unsupported_kwargs("OpenAI Responses input token count", unsupported) + params: dict[str, Any] = { + "model": self._model_name, + "input": normalize_responses_input( + request.prompt, + request.system_prompt, + request.messages, + provider=getattr(self, "_provider", "openai"), + ), + } + if request.system_prompt and request.messages is None: + params["instructions"] = request.system_prompt + if request.conversation is not None: + params["conversation"] = request.conversation + if request.tools: + params["tools"] = self._convert_responses_tools(request.tools) + if request.tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice(request.tool_choice) + if request.parallel_tool_calls is not None: + params["parallel_tool_calls"] = request.parallel_tool_calls + if request.reasoning is not None: + params["reasoning"] = request.reasoning + if request.previous_response_id is not None: + params["previous_response_id"] = request.previous_response_id + if request.personality is not None: + params["personality"] = request.personality + if request.response_format is not None: + params["text"] = self._convert_responses_format(request.response_format) + if request.truncation is not None: + params["truncation"] = request.truncation + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.extra_body is not None: + params["extra_body"] = request.extra_body + if request.timeout is not None: + params["timeout"] = request.timeout + return params + + def count_input_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible count_input_tokens") + self._require_responses_resource("count_input_tokens") + params = self._build_token_count_request(request) + response = self._call_sdk_resource( + self._get_client().responses.input_tokens.count, + kwargs=params, + operation="OpenAI Responses 输入 token 统计", + ) + value = self._field(response, "input_tokens") + if value is None: + raise RuntimeError("OpenAI token count 响应缺少 input_tokens。") + return int(value) + + def create_vector_store(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.create, + kwargs=kwargs, + operation="OpenAI Vector Store 创建", + ) + + def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.retrieve, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 读取", + ) + + def list_vector_stores(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.list, + kwargs=kwargs, + operation="OpenAI Vector Store 列表", + ) + + def search_vector_store(self, vector_store_id: str, query: str | list[str], **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.search, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"query": query}, kwargs, "OpenAI Vector Store 搜索" + ), + operation="OpenAI Vector Store 搜索", + ) + + def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.delete, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 删除", + ) + + def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.update, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 更新", + ) + + def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.create, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"file_id": file_id}, kwargs, "OpenAI Vector Store 文件创建" + ), + operation="OpenAI Vector Store 文件创建", + ) + + def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.create_and_poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件创建轮询", + ), + operation="OpenAI Vector Store 文件创建轮询", + ) + + def upload_vector_store_file( + self, + vector_store_id: str, + file: Any, + *, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传并挂载向量库文件,仅转发 SDK 支持的分块策略。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传", kwargs) + upload_kwargs = ( + {"chunking_strategy": chunking_strategy} + if chunking_strategy is not None + else {} + ) + return self._call_sdk_resource( + self._get_client().vector_stores.files.upload, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传", + ), + operation="OpenAI Vector Store 文件上传", + ) + + def upload_vector_store_file_and_poll( + self, + vector_store_id: str, + file: Any, + *, + attributes: Mapping[str, str | float | bool] | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传单个文件到向量库并等待处理完成。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传轮询", kwargs) + upload_kwargs = { + key: value + for key, value in { + "attributes": dict(attributes) if attributes is not None else None, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().vector_stores.files.upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传轮询", + ), + operation="OpenAI Vector Store 文件上传轮询", + ) + + def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件读取", + ), + operation="OpenAI Vector Store 文件读取", + ) + + def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.list, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件列表", + ) + + def update_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + attributes: Mapping[str, str | float | bool] | None, + extra_headers: Mapping[str, str] | None = None, + extra_query: Mapping[str, Any] | None = None, + extra_body: Mapping[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """更新向量库文件属性,严格遵循 SDK 的必填 ``attributes`` 契约。""" + reject_unsupported_kwargs("OpenAI 向量库文件更新", kwargs) + request = { + "vector_store_id": vector_store_id, + "attributes": dict(attributes) if attributes is not None else None, + } + request.update( + { + key: value + for key, value in { + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + ) + return self._call_sdk_resource( + self._get_client().vector_stores.files.update, + args=(file_id,), + kwargs=request, + operation="OpenAI Vector Store 文件更新", + ) + + def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件删除", + ), + operation="OpenAI Vector Store 文件删除", + ) + + def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.content, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件内容读取", + ), + operation="OpenAI Vector Store 文件内容读取", + ) + + def poll_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询向量库文件,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件轮询", kwargs) + return self._call_sdk_resource( + self._get_client().vector_stores.files.poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件轮询", + ), + operation="OpenAI Vector Store 文件轮询", + ) + + def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.create, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建", + ) + + def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.create_and_poll, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建轮询", + ) + + def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.retrieve, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次读取", + ), + operation="OpenAI Vector Store 文件批次读取", + ) + + def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.cancel, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次取消", + ), + operation="OpenAI Vector Store 文件批次取消", + ) + + def poll_vector_store_file_batch( + self, + vector_store_id: str, + batch_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询向量库文件批次,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次轮询", kwargs) + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.poll, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件批次轮询", + ), + operation="OpenAI Vector Store 文件批次轮询", + ) + + def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.list_files, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次文件列表", + ), + operation="OpenAI Vector Store 文件批次文件列表", + ) + + def upload_vector_store_file_batch_and_poll( + self, + vector_store_id: str, + files: Any, + *, + max_concurrency: int | None = None, + file_ids: Any | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传并轮询向量库文件批次,严格匹配 SDK 辅助方法参数。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次上传轮询", kwargs) + poll_kwargs = { + key: value + for key, value in { + "max_concurrency": max_concurrency, + "file_ids": file_ids, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + upload_and_poll = cast(Any, self._get_client().vector_stores.file_batches.upload_and_poll) + return self._call_sdk_resource( + upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "files": files}, + poll_kwargs, + "OpenAI Vector Store 文件批次上传轮询", + ), + operation="OpenAI Vector Store 文件批次上传轮询", + ) + + def list_models(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.list, kwargs=kwargs, operation="OpenAI 模型列表" + ) + + def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.retrieve, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型读取", + ) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.delete, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型删除", + ) + + def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().moderations.create, + kwargs=self._merge_resource_kwargs( + {"input": input}, kwargs, "OpenAI Moderation 创建" + ), + operation="OpenAI Moderation 创建", + ) + + def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.generate, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 图片生成" + ), + operation="OpenAI 图片生成", + ) + + def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.edit, + kwargs=self._merge_resource_kwargs( + {"image": image, "prompt": prompt}, kwargs, "OpenAI 图片编辑" + ), + operation="OpenAI 图片编辑", + ) + + def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.create_variation, + kwargs=self._merge_resource_kwargs( + {"image": image}, kwargs, "OpenAI 图片变体" + ), + operation="OpenAI 图片变体", + ) + + def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.speech.create, + kwargs=self._merge_resource_kwargs( + {"input": text, "model": model, "voice": voice}, + kwargs, + "OpenAI 语音合成", + ), + operation="OpenAI 语音合成", + ) + + def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.transcriptions.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频转写" + ), + operation="OpenAI 音频转写", + ) + + def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.translations.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频翻译" + ), + operation="OpenAI 音频翻译", + ) + + def create_video(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create, kwargs=kwargs, operation="OpenAI 视频创建" + ) + + def create_video_and_poll(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create_and_poll, + kwargs=kwargs, + operation="OpenAI 视频创建轮询", + ) + + def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.retrieve, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频读取", + ) + + def list_videos(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.list, kwargs=kwargs, operation="OpenAI 视频列表" + ) + + def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.delete, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频删除", + ) + + def download_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.download_content, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频下载", + ) + + def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create_character, + kwargs=self._merge_resource_kwargs( + {"name": name, "video": video}, kwargs, "OpenAI 视频角色创建" + ), + operation="OpenAI 视频角色创建", + ) + + def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.get_character, + args=(character_id,), + kwargs=kwargs, + operation="OpenAI 视频角色读取", + ) + + def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.edit, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "video": video}, kwargs, "OpenAI 视频编辑" + ), + operation="OpenAI 视频编辑", + ) + + def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.extend, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "seconds": seconds, "video": video}, + kwargs, + "OpenAI 视频续写", + ), + operation="OpenAI 视频续写", + ) + + def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.remix, + args=(video_id,), + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 视频混剪" + ), + operation="OpenAI 视频混剪", + ) + + def poll_video( + self, + video_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询视频处理状态,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 视频轮询", kwargs) + return self._call_sdk_resource( + self._get_client().videos.poll, + args=(video_id,), + kwargs={ + "poll_interval_ms": poll_interval_ms, + } if poll_interval_ms is not None else {}, + operation="OpenAI 视频轮询", + ) + + def create_upload(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.create, + kwargs=kwargs, + operation="OpenAI Upload 创建", + ) + + def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.complete, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 完成", + ) + + def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.cancel, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 取消", + ) + + def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.parts.create, + args=(upload_id,), + kwargs=self._merge_resource_kwargs( + {"data": data}, kwargs, "OpenAI Upload 分片创建" + ), + operation="OpenAI Upload 分片创建", + ) + + def upload_file_chunked( + self, + *, + file: Any, + mime_type: str, + purpose: str, + filename: str | None = None, + bytes: int | None = None, + part_size: int | None = None, + md5: str | None = None, + **kwargs: Any, + ) -> Any: + """按 SDK 的窄参数契约执行分片上传。""" + reject_unsupported_kwargs("OpenAI 分片文件上传", kwargs) + request = { + key: value + for key, value in { + "file": file, + "mime_type": mime_type, + "purpose": purpose, + "filename": filename, + "bytes": bytes, + "part_size": part_size, + "md5": md5, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().uploads.upload_file_chunked, + kwargs=request, + operation="OpenAI 分片文件上传", + ) + + def compact_responses( + self, + *, + model: str | None = None, + input: Any = None, + instructions: str | None = None, + previous_response_id: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: Any = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + extra_body: dict[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """压缩 Responses 会话上下文,严格匹配 OpenAI SDK 参数。""" + self._require_responses_resource("compact_responses") + reject_unsupported_kwargs("OpenAI Responses compact", kwargs) + compact_kwargs = { + key: value + for key, value in { + "model": model or self._model_name, + "input": input, + "instructions": instructions, + "previous_response_id": previous_response_id, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "service_tier": service_tier, + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().responses.compact, + kwargs=compact_kwargs, + operation="OpenAI Responses compact", + ) + + def create_conversation(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.create, + kwargs=kwargs, + operation="OpenAI Conversation 创建", + ) + + def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.retrieve, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 读取", + ) + + def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.update, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 更新", + ) + + def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.delete, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 删除", + ) + + def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.list, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 项目列表", + ) + + def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.create, + args=(conversation_id,), + kwargs=self._merge_resource_kwargs( + {"items": items}, kwargs, "OpenAI Conversation 项目创建" + ), + operation="OpenAI Conversation 项目创建", + ) + + def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.retrieve, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目读取", + ), + operation="OpenAI Conversation 项目读取", + ) + + def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.delete, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目删除", + ), + operation="OpenAI Conversation 项目删除", + ) + + def create_container(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.create, + kwargs=kwargs, + operation="OpenAI Container 创建", + ) + + def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.retrieve, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 读取", + ) + + def list_containers(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.list, + kwargs=kwargs, + operation="OpenAI Container 列表", + ) + + def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.delete, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 删除", + ) + + def create_container_file( + self, + container_id: str, + file: Any = None, + file_id: str | None = None, + **kwargs: Any, + ) -> Any: + file_kwargs = { + key: value + for key, value in {"file": file, "file_id": file_id}.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().containers.files.create, + args=(container_id,), + kwargs=self._merge_resource_kwargs( + file_kwargs, + kwargs, + "OpenAI Container 文件创建", + ), + operation="OpenAI Container 文件创建", + ) + + def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.list, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 文件列表", + ) + + def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件读取", + ), + operation="OpenAI Container 文件读取", + ) + + def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件删除", + ), + operation="OpenAI Container 文件删除", + ) + + def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + files = getattr(getattr(self._get_client(), "containers", None), "files", None) + content = getattr(files, "content", None) + method = getattr(content, "retrieve", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供容器文件内容读取资源 " + "(containers.files.content.retrieve)。" + ) + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件内容读取", + ), + operation="OpenAI Container 文件内容读取", + ) + + def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.create, + kwargs=kwargs, + operation="OpenAI Fine-tuning 创建", + ) + + def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.list, + kwargs=kwargs, + operation="OpenAI Fine-tuning 列表", + ) + + def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.retrieve, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 读取", + ) + + def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.cancel, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 取消", + ) + + def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.pause, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 暂停", + ) + + def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.resume, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 恢复", + ) + + def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.list_events, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 事件列表", + ) + + def create_eval(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.create, + kwargs=kwargs, + operation="OpenAI Eval 创建", + ) + + def list_evals(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.list, + kwargs=kwargs, + operation="OpenAI Eval 列表", + ) + + def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.retrieve, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 读取", + ) + + def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.update, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 更新", + ) + + def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.delete, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 删除", + ) + + def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.create, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 创建", + ) + + def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.list, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 列表", + ) + + def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.retrieve, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 读取" + ), + operation="OpenAI Eval Run 读取", + ) + + def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.cancel, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 取消" + ), + operation="OpenAI Eval Run 取消", + ) + + def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.delete, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 删除" + ), + operation="OpenAI Eval Run 删除", + ) + + def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.output_items.list, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, + kwargs, + "OpenAI Eval Run 输出项列表", + ), + operation="OpenAI Eval Run 输出项列表", + ) + + def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.output_items.retrieve, + args=(output_item_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id, "run_id": run_id}, + kwargs, + "OpenAI Eval Run 输出项读取", + ), + operation="OpenAI Eval Run 输出项读取", + ) + + async def async_upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "purpose": cast(Any, purpose)}, + kwargs, + "OpenAI 文件上传", + ), + operation="OpenAI 文件上传", + ) + + async def async_list_files(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.list, kwargs=kwargs, operation="OpenAI 文件列表" + ) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.retrieve, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件读取", + ) + + async def async_file_content(self, file_id: str, **kwargs: Any) -> Any: + files = getattr(self._get_aclient(), "files", None) + method = getattr(files, "content", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供文件内容读取资源(files.content)。" + ) + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件内容读取", + ) + + async def async_retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + files = self._get_aclient().files + method = getattr(files, "retrieve_content", None) + if method is not None: + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件文本读取", + ) + return await self.async_file_content(file_id, **kwargs) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.delete, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件删除", + ) + + async def async_wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """异步等待文件处理完成,仅转发 AsyncOpenAI SDK 的轮询参数。""" + reject_unsupported_kwargs("OpenAI 文件等待", kwargs) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().files.wait_for_processing, + kwargs=self._merge_resource_kwargs( + {"id": file_id}, poll_kwargs, "OpenAI 文件等待" + ), + operation="OpenAI 文件等待", + ) + + async def async_create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + if self._protocol == "responses" and ( + endpoint is None or endpoint.rstrip("/") == "/v1/responses" + ): + self._require_responses_resource("create_batch") + if endpoint is None: + endpoint = "/v1/responses" if self._protocol == "responses" else "/v1/chat/completions" + kwargs.setdefault("completion_window", "24h") + return await self._call_async_sdk_resource( + self._get_aclient().batches.create, + kwargs=self._merge_resource_kwargs( + { + "input_file_id": input_file_id, + "endpoint": cast(Any, endpoint), + }, + kwargs, + "OpenAI Batch 创建", + ), + operation="OpenAI Batch 创建", + ) + + async def async_retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.retrieve, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 读取", + ) + + async def async_cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.cancel, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 取消", + ) + + async def async_list_batches(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.list, kwargs=kwargs, operation="OpenAI Batch 列表" + ) + + async def async_retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.retrieve, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 读取", + ) + + async def async_cancel_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("cancel_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.cancel, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 取消", + ) + + async def async_delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.delete, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 删除", + ) + + async def async_list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + return await self._call_async_sdk_resource( + self._get_aclient().responses.input_items.list, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 输入项列表", + ) + + def async_connect_responses(self, **kwargs: Any) -> Any: + """返回 AsyncOpenAI Responses WebSocket 连接管理器。""" + self._require_responses_resource("connect_responses") + return self._call_sdk_resource( + self._get_aclient().responses.connect, + kwargs=kwargs, + operation="OpenAI Responses 连接", + ) + + def async_stream_responses(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 AsyncOpenAI Responses 流上下文管理器。""" + self._require_responses_resource("stream_responses") + if request is not None and kwargs: + raise ValueError("async_stream_responses 不能同时传 request 和原生关键字参数。") + if request is not None: + params = self._build_responses_request(**request.copy_with(stream=True).to_invoke_kwargs()) + params.pop("stream", None) + else: + params = dict(kwargs) + params = self._prepare_responses_stream_params(params) + return self._call_sdk_resource( + self._get_aclient().responses.stream, + kwargs=params, + operation="OpenAI Responses 流", + ) + + async def async_count_input_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible async_count_input_tokens") + self._require_responses_resource("count_input_tokens") + params = self._build_token_count_request(request) + response = await self._call_async_sdk_resource( + self._get_aclient().responses.input_tokens.count, + kwargs=params, + operation="OpenAI Responses 输入 token 统计", + ) + value = self._field(response, "input_tokens") + if value is None: + raise RuntimeError("OpenAI token count 响应缺少 input_tokens。") + return int(value) + + async def async_create_vector_store(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.create, + kwargs=kwargs, + operation="OpenAI Vector Store 创建", + ) + + async def async_retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.retrieve, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 读取", + ) + + async def async_list_vector_stores(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.list, + kwargs=kwargs, + operation="OpenAI Vector Store 列表", + ) + + async def async_search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.search, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"query": query}, kwargs, "OpenAI Vector Store 搜索" + ), + operation="OpenAI Vector Store 搜索", + ) + + async def async_delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.delete, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 删除", + ) + + async def async_update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.update, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 更新", + ) + + async def async_create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.create, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"file_id": file_id}, kwargs, "OpenAI Vector Store 文件创建" + ), + operation="OpenAI Vector Store 文件创建", + ) + + async def async_create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.create_and_poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件创建轮询", + ), + operation="OpenAI Vector Store 文件创建轮询", + ) + + async def async_upload_vector_store_file( + self, + vector_store_id: str, + file: Any, + *, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传并挂载向量库文件,仅转发 SDK 支持的分块策略。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传", kwargs) + upload_kwargs = ( + {"chunking_strategy": chunking_strategy} + if chunking_strategy is not None + else {} + ) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.upload, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传", + ), + operation="OpenAI Vector Store 文件上传", + ) + + async def async_upload_vector_store_file_and_poll( + self, + vector_store_id: str, + file: Any, + *, + attributes: Mapping[str, str | float | bool] | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传单个文件到向量库并等待处理完成。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传轮询", kwargs) + upload_kwargs = { + key: value + for key, value in { + "attributes": dict(attributes) if attributes is not None else None, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传轮询", + ), + operation="OpenAI Vector Store 文件上传轮询", + ) + + async def async_retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件读取", + ), + operation="OpenAI Vector Store 文件读取", + ) + + async def async_list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.list, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件列表", + ) + + async def async_update_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + attributes: Mapping[str, str | float | bool] | None, + extra_headers: Mapping[str, str] | None = None, + extra_query: Mapping[str, Any] | None = None, + extra_body: Mapping[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """异步更新向量库文件属性,严格遵循 SDK 的必填 ``attributes`` 契约。""" + reject_unsupported_kwargs("OpenAI 向量库文件更新", kwargs) + request = { + "vector_store_id": vector_store_id, + "attributes": dict(attributes) if attributes is not None else None, + } + request.update( + { + key: value + for key, value in { + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + ) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.update, + args=(file_id,), + kwargs=request, + operation="OpenAI Vector Store 文件更新", + ) + + async def async_delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件删除", + ), + operation="OpenAI Vector Store 文件删除", + ) + + async def async_vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.content, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件内容读取", + ), + operation="OpenAI Vector Store 文件内容读取", + ) + + async def async_poll_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询向量库文件,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件轮询", + ), + operation="OpenAI Vector Store 文件轮询", + ) + + async def async_create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.create, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建", + ) + + async def async_create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.create_and_poll, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建轮询", + ) + + async def async_retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.retrieve, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次读取", + ), + operation="OpenAI Vector Store 文件批次读取", + ) + + async def async_cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.cancel, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次取消", + ), + operation="OpenAI Vector Store 文件批次取消", + ) + + async def async_poll_vector_store_file_batch( + self, + vector_store_id: str, + batch_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询向量库文件批次,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.poll, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件批次轮询", + ), + operation="OpenAI Vector Store 文件批次轮询", + ) + + async def async_list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.list_files, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次文件列表", + ), + operation="OpenAI Vector Store 文件批次文件列表", + ) + + async def async_upload_vector_store_file_batch_and_poll( + self, + vector_store_id: str, + files: Any, + *, + max_concurrency: int | None = None, + file_ids: Any | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传并轮询向量库文件批次,严格匹配 SDK 参数。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次上传轮询", kwargs) + poll_kwargs = { + key: value + for key, value in { + "max_concurrency": max_concurrency, + "file_ids": file_ids, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + upload_and_poll = cast( + Any, self._get_aclient().vector_stores.file_batches.upload_and_poll + ) + return await self._call_async_sdk_resource( + upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "files": files}, + poll_kwargs, + "OpenAI Vector Store 文件批次上传轮询", + ), + operation="OpenAI Vector Store 文件批次上传轮询", + ) + + async def async_list_models(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.list, kwargs=kwargs, operation="OpenAI 模型列表" + ) + + async def async_retrieve_model(self, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.retrieve, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型读取", + ) + + async def async_delete_model(self, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.delete, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型删除", + ) + + async def async_create_moderation(self, input: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().moderations.create, + kwargs=self._merge_resource_kwargs( + {"input": input}, kwargs, "OpenAI Moderation 创建" + ), + operation="OpenAI Moderation 创建", + ) + + async def async_generate_image(self, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.generate, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 图片生成" + ), + operation="OpenAI 图片生成", + ) + + async def async_edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.edit, + kwargs=self._merge_resource_kwargs( + {"image": image, "prompt": prompt}, kwargs, "OpenAI 图片编辑" + ), + operation="OpenAI 图片编辑", + ) + + async def async_create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.create_variation, + kwargs=self._merge_resource_kwargs( + {"image": image}, kwargs, "OpenAI 图片变体" + ), + operation="OpenAI 图片变体", + ) + + async def async_text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.speech.create, + kwargs=self._merge_resource_kwargs( + {"input": text, "model": model, "voice": voice}, + kwargs, + "OpenAI 语音合成", + ), + operation="OpenAI 语音合成", + ) + + async def async_transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.transcriptions.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频转写" + ), + operation="OpenAI 音频转写", + ) + + async def async_translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.translations.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频翻译" + ), + operation="OpenAI 音频翻译", + ) + + async def async_create_video(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create, + kwargs=kwargs, + operation="OpenAI 视频创建", + ) + + async def async_create_video_and_poll(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create_and_poll, + kwargs=kwargs, + operation="OpenAI 视频创建轮询", + ) + + async def async_retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.retrieve, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频读取", + ) + + async def async_list_videos(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.list, kwargs=kwargs, operation="OpenAI 视频列表" + ) + + async def async_delete_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.delete, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频删除", + ) + + async def async_download_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.download_content, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频下载", + ) + + async def async_create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create_character, + kwargs=self._merge_resource_kwargs( + {"name": name, "video": video}, kwargs, "OpenAI 视频角色创建" + ), + operation="OpenAI 视频角色创建", + ) + + async def async_retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.get_character, + args=(character_id,), + kwargs=kwargs, + operation="OpenAI 视频角色读取", + ) + + async def async_edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.edit, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "video": video}, kwargs, "OpenAI 视频编辑" + ), + operation="OpenAI 视频编辑", + ) + + async def async_extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.extend, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "seconds": seconds, "video": video}, + kwargs, + "OpenAI 视频续写", + ), + operation="OpenAI 视频续写", + ) + + async def async_remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.remix, + args=(video_id,), + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 视频混剪" + ), + operation="OpenAI 视频混剪", + ) + + async def async_poll_video( + self, + video_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询视频处理状态,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 视频轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().videos.poll, + args=(video_id,), + kwargs={ + "poll_interval_ms": poll_interval_ms, + } if poll_interval_ms is not None else {}, + operation="OpenAI 视频轮询", + ) + + async def async_create_upload(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.create, + kwargs=kwargs, + operation="OpenAI Upload 创建", + ) + + async def async_complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.complete, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 完成", + ) + + async def async_cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.cancel, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 取消", + ) + + async def async_create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.parts.create, + args=(upload_id,), + kwargs=self._merge_resource_kwargs( + {"data": data}, kwargs, "OpenAI Upload 分片创建" + ), + operation="OpenAI Upload 分片创建", + ) + + async def async_upload_file_chunked( + self, + *, + file: Any, + mime_type: str, + purpose: str, + filename: str | None = None, + bytes: int | None = None, + part_size: int | None = None, + md5: str | None = None, + **kwargs: Any, + ) -> Any: + """异步分片上传,严格匹配 SDK 的窄参数契约。""" + reject_unsupported_kwargs("OpenAI 分片文件上传", kwargs) + request = { + key: value + for key, value in { + "file": file, + "mime_type": mime_type, + "purpose": purpose, + "filename": filename, + "bytes": bytes, + "part_size": part_size, + "md5": md5, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().uploads.upload_file_chunked, + kwargs=request, + operation="OpenAI 分片文件上传", + ) + + async def async_compact_responses( + self, + *, + model: str | None = None, + input: Any = None, + instructions: str | None = None, + previous_response_id: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: Any = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + extra_body: dict[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """异步压缩 Responses 会话,严格匹配 AsyncOpenAI SDK 参数。""" + self._require_responses_resource("compact_responses") + reject_unsupported_kwargs("OpenAI Responses compact", kwargs) + compact_kwargs = { + key: value + for key, value in { + "model": model or self._model_name, + "input": input, + "instructions": instructions, + "previous_response_id": previous_response_id, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "service_tier": service_tier, + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().responses.compact, + kwargs=compact_kwargs, + operation="OpenAI Responses compact", + ) + + async def async_create_conversation(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.create, + kwargs=kwargs, + operation="OpenAI Conversation 创建", + ) + + async def async_retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.retrieve, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 读取", + ) + + async def async_update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.update, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 更新", + ) + + async def async_delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.delete, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 删除", + ) + + async def async_list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.list, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 项目列表", + ) + + async def async_create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.create, + args=(conversation_id,), + kwargs=self._merge_resource_kwargs( + {"items": items}, kwargs, "OpenAI Conversation 项目创建" + ), + operation="OpenAI Conversation 项目创建", + ) + + async def async_retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.retrieve, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目读取", + ), + operation="OpenAI Conversation 项目读取", + ) + + async def async_delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.delete, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目删除", + ), + operation="OpenAI Conversation 项目删除", + ) + + async def async_create_container(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.create, + kwargs=kwargs, + operation="OpenAI Container 创建", + ) + + async def async_retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.retrieve, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 读取", + ) + + async def async_list_containers(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.list, + kwargs=kwargs, + operation="OpenAI Container 列表", + ) + + async def async_delete_container(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.delete, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 删除", + ) + + async def async_create_container_file( + self, + container_id: str, + file: Any = None, + file_id: str | None = None, + **kwargs: Any, + ) -> Any: + file_kwargs = { + key: value + for key, value in {"file": file, "file_id": file_id}.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.create, + args=(container_id,), + kwargs=self._merge_resource_kwargs( + file_kwargs, + kwargs, + "OpenAI Container 文件创建", + ), + operation="OpenAI Container 文件创建", + ) + + async def async_list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.list, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 文件列表", + ) + + async def async_retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件读取", + ), + operation="OpenAI Container 文件读取", + ) + + async def async_delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件删除", + ), + operation="OpenAI Container 文件删除", + ) + + async def async_container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + files = getattr(getattr(self._get_aclient(), "containers", None), "files", None) + content = getattr(files, "content", None) + method = getattr(content, "retrieve", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供容器文件内容读取资源 " + "(containers.files.content.retrieve)。" + ) + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件内容读取", + ), + operation="OpenAI Container 文件内容读取", + ) + + async def async_create_fine_tuning_job(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.create, + kwargs=kwargs, + operation="OpenAI Fine-tuning 创建", + ) + + async def async_list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.list, + kwargs=kwargs, + operation="OpenAI Fine-tuning 列表", + ) + + async def async_retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.retrieve, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 读取", + ) + + async def async_cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.cancel, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 取消", + ) + + async def async_pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.pause, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 暂停", + ) + + async def async_resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.resume, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 恢复", + ) + + async def async_list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.list_events, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 事件列表", + ) + + async def async_create_eval(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.create, + kwargs=kwargs, + operation="OpenAI Eval 创建", + ) + + async def async_list_evals(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.list, + kwargs=kwargs, + operation="OpenAI Eval 列表", + ) + + async def async_retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.retrieve, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 读取", + ) + + async def async_update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.update, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 更新", + ) + + async def async_delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.delete, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 删除", + ) + + async def async_create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.create, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 创建", + ) + + async def async_list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.list, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 列表", + ) + + async def async_retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.retrieve, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 读取" + ), + operation="OpenAI Eval Run 读取", + ) + + async def async_cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.cancel, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 取消" + ), + operation="OpenAI Eval Run 取消", + ) + + async def async_delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.delete, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 删除" + ), + operation="OpenAI Eval Run 删除", + ) + + async def async_list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.output_items.list, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, + kwargs, + "OpenAI Eval Run 输出项列表", + ), + operation="OpenAI Eval Run 输出项列表", + ) + + async def async_retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.output_items.retrieve, + args=(output_item_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id, "run_id": run_id}, + kwargs, + "OpenAI Eval Run 输出项读取", + ), + operation="OpenAI Eval Run 输出项读取", + ) + + def close(self) -> None: + """释放同步和异步 OpenAI SDK 客户端。 + + 同步上下文中会桥接等待异步客户端的关闭;活动事件循环中则显式 + 要求调用 ``aclose``,并保留未关闭的异步客户端供后续清理。 + """ + client = self._client + async_client = self._aclient + self._client = None + first_error: Exception | None = None + closed_ids: set[int] = set() + if client is not None: + try: + client.close() + closed_ids.add(id(client)) + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + if id(async_client) in closed_ids: + self._aclient = None + else: + try: + close_resource_sync(async_client, "OpenAI 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放同步和异步 OpenAI SDK 客户端,即使其中一个关闭失败。""" + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error diff --git a/src/providers/qwen.py b/src/providers/qwen.py index 3585ee8..e9fa2ac 100644 --- a/src/providers/qwen.py +++ b/src/providers/qwen.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class QwenProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="qwen") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="qwen", protocol=protocol, options=options) diff --git a/src/providers/siliconflow.py b/src/providers/siliconflow.py index d9506c4..67e3d58 100644 --- a/src/providers/siliconflow.py +++ b/src/providers/siliconflow.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class SiliconflowProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="siliconflow") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="siliconflow", protocol=protocol, options=options) diff --git a/src/providers/siliconflow_rerank.py b/src/providers/siliconflow_rerank.py index 8842757..3c457b8 100644 --- a/src/providers/siliconflow_rerank.py +++ b/src/providers/siliconflow_rerank.py @@ -1,13 +1,18 @@ -# -*- coding: utf-8 -*- +import math import time -from typing import Any, Dict, List, Tuple, cast +from collections.abc import Mapping +from typing import Any -import requests -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential -from src.providers.__base__.model_provider import RerankModel +from src.providers.__base__.model_provider import ( + RerankModel, + is_retryable_error, + validate_secret_free_options, +) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import redact_sensitive_text, validate_secret_free_payload logger = get_module_logger(__name__) @@ -17,8 +22,14 @@ class SiliconflowRerankProvider(RerankModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str): + _OPTION_KEYS = frozenset({"return_documents", "truncate", "extra_body", "timeout"}) + + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + self._options = validate_secret_free_options(options, "SiliconFlow Rerank") + unknown = sorted(set(self._options).difference(self._OPTION_KEYS)) + if unknown: + raise ValueError("SiliconFlow Rerank options 包含不支持的字段: " + ", ".join(unknown)) settings = get_settings() self._api_key = settings.siliconflow_api_key self._base_url = settings.siliconflow_base_url @@ -32,36 +43,123 @@ def __init__(self, model_name: str): logger.info(f"初始化 SiliconflowRerankProvider,模型: {model_name}") - def _get_headers(self) -> Dict[str, str]: + def _get_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - def _prepare_payload(self, query: str, documents: List[str], top_n: int) -> Dict[str, Any]: - return { + def _prepare_payload(self, query: str, documents: list[str], top_n: int) -> dict[str, Any]: + self._validate_inputs(query, documents, top_n) + payload: dict[str, Any] = { "query": query, "documents": documents, "model": self._model_name, "top_n": top_n, "return_documents": True } + options = dict(self._options) + options.pop("timeout", None) + extra_body = validate_secret_free_payload( + options.pop("extra_body", None), + "SiliconFlow Rerank", + "options.extra_body", + ) + overlap = sorted(set(payload).intersection(options)) + if overlap: + raise ValueError( + "SiliconFlow Rerank options 不允许覆盖请求字段: " + ", ".join(overlap) + ) + if extra_body: + option_overlap = sorted(set(options).intersection(extra_body)) + if option_overlap: + raise ValueError( + "SiliconFlow Rerank options 与 options.extra_body 重复: " + + ", ".join(option_overlap) + ) + body_overlap = sorted(set(payload).intersection(extra_body)) + if body_overlap: + raise ValueError( + "SiliconFlow Rerank options.extra_body 不允许覆盖请求字段: " + + ", ".join(body_overlap) + ) + options.update(dict(extra_body)) + payload.update(options) + return payload + + def _request_timeout(self) -> float: + timeout = self._options.get("timeout", 30.0) + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("SiliconFlow Rerank options.timeout 必须是正数。") + return float(timeout) - def _parse_response(self, results: List[Dict], documents: List[str]) -> Tuple[List[int], List[float]]: - content_to_index = {content: i for i, content in enumerate(documents)} + @staticmethod + def _validate_inputs(query: str, documents: list[str], top_n: int) -> None: + if not isinstance(query, str) or not query.strip(): + raise ValueError("SiliconFlow Rerank query 必须是非空字符串。") + if not isinstance(documents, list) or not documents: + raise ValueError("SiliconFlow Rerank documents 必须是非空列表。") + if any(not isinstance(document, str) for document in documents): + raise ValueError("SiliconFlow Rerank documents 必须全部是字符串。") + if not isinstance(top_n, int) or isinstance(top_n, bool) or top_n < 1: + raise ValueError("SiliconFlow Rerank top_n 必须是大于等于 1 的整数。") + + def _parse_response(self, results: list[dict], documents: list[str]) -> tuple[list[int], list[float]]: + positions: dict[str, list[int]] = {} + for index, content in enumerate(documents): + positions.setdefault(content, []).append(index) indices = [] scores = [] + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") for res in results: - doc_content = res.get("document", {}).get("text") if isinstance(res.get("document"), dict) else res.get("document") - if doc_content in content_to_index: - indices.append(content_to_index[doc_content]) - scores.append(res.get("relevance_score", 0.0)) + if not isinstance(res, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应包含无效结果项。") + if "index" in res: + direct_index = res["index"] + if ( + not isinstance(direct_index, int) + or isinstance(direct_index, bool) + or not 0 <= direct_index < len(documents) + ): + raise RuntimeError("SiliconFlow Rerank 响应 index 无效。") + if direct_index in indices: + raise RuntimeError("SiliconFlow Rerank 响应包含重复 index。") + index = direct_index + document = res.get("document") + document_text = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if document_text is not None and document_text != documents[index]: + raise RuntimeError("SiliconFlow Rerank 响应 index 与 document 不匹配。") + positions.get(documents[index], []).remove(index) + else: + document = res.get("document") + doc_content = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if not isinstance(doc_content, str) or not positions.get(doc_content): + raise RuntimeError("SiliconFlow Rerank 响应无法映射到输入文档。") + index = positions[doc_content].pop(0) + try: + score = float(res["relevance_score"]) + except (KeyError, TypeError, ValueError) as exc: + raise RuntimeError("SiliconFlow Rerank 响应 relevance_score 无效。") from exc + if not math.isfinite(score): + raise RuntimeError("SiliconFlow Rerank 响应 relevance_score 必须是有限数值。") + indices.append(index) + scores.append(score) return indices, scores @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步 Rerank (CSE Sensor)。""" logger.info(f"调用 SiliconFlow Rerank ({self._model_name}),文档数: {len(documents)}") import httpx @@ -69,30 +167,40 @@ def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int url = f"{self._base_url.rstrip('/')}/rerank" try: - with httpx.Client(timeout=30.0) as client: + with httpx.Client(timeout=self._request_timeout()) as client: response = client.post( url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"SiliconFlow Rerank ({self._model_name}) 完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"SiliconFlow Rerank ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "SiliconFlow Rerank (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步 Rerank (CSE Sensor)。""" logger.info(f"异步调用 SiliconFlow Rerank ({self._model_name}),文档数: {len(documents)}") import httpx @@ -100,19 +208,29 @@ async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[L url = f"{self._base_url.rstrip('/')}/rerank" try: - async with httpx.AsyncClient(timeout=30.0) as aclient: + async with httpx.AsyncClient(timeout=self._request_timeout()) as aclient: response = await aclient.post( url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"SiliconFlow Rerank ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"SiliconFlow Rerank ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "SiliconFlow Rerank (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/volcengine.py b/src/providers/volcengine.py index 307496d..d6d53c1 100644 --- a/src/providers/volcengine.py +++ b/src/providers/volcengine.py @@ -1,172 +1,2341 @@ -import time -from typing import Any, AsyncGenerator, Dict, Generator, List, Optional +import inspect +import math +from collections.abc import AsyncGenerator, Collection, Generator, Mapping +from typing import Any -from volcenginesdkarkruntime import Ark, AsyncArk -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + content_to_text, + field, + is_retryable_error, + merge_tool_call_fragment, + normalize_embedding_vector, + normalize_messages, + normalize_responses_input, + normalize_tool_arguments, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, + validate_secret_free_options, + validate_secret_free_request_overrides, ) +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import ArkResources, AsyncArkResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import validate_secret_free_payload logger = get_module_logger(__name__) + +def _load_ark_clients() -> tuple[type[Any], type[Any]]: + """按需加载 Ark SDK,避免导入 provider 时扫描整个生成式类型树。""" + try: + from volcenginesdkarkruntime import ( # type: ignore[import-untyped] + Ark, + AsyncArk, + ) + except ImportError as exc: + raise RuntimeError( + "未安装 volcengine-python-sdk[ark],无法使用 VolcengineProvider。" + ) from exc + return Ark, AsyncArk + + class VolcengineProvider(LargeLanguageModel, TextEmbeddingModel): - """ - 火山引擎模型提供商 (豆包 Ark SDK)。 - 已注入 CSE 性能传感器与 tenacity 重试机制。 - """ + """火山引擎 Ark Provider,支持 Chat Completions、Responses 和 Embeddings。""" + + _ARK_CHAT_MODEL_OPTION_KEYS = frozenset( + { + "frequency_penalty", "function_call", "logit_bias", "logprobs", + "max_completion_tokens", "max_tokens", "n", "parallel_tool_calls", + "presence_penalty", "reasoning_effort", "repetition_penalty", + "response_format", "service_tier", "stop", "stream_options", + "temperature", "thinking", "tool_choice", "top_logprobs", "top_p", + "user", "extra_body", "top_k", "seed", + } + ) + _ARK_RESPONSES_MODEL_OPTION_KEYS = frozenset( + { + "caching", "context_management", "conversation", "expire_at", + "extra_body", "frequency_penalty", "max_completion_tokens", + "max_output_tokens", "max_tokens", "max_tool_calls", + "parallel_tool_calls", "previous_response_id", "presence_penalty", + "reasoning", "reasoning_effort", "response_format", "service_tier", + "session", "store", "temperature", "text", "thinking", "tool_choice", + "top_k", "top_p", "seed", + } + ) + _ARK_RESPONSES_CREATE_KEYS = frozenset( + { + "input", "model", "instructions", "max_output_tokens", + "parallel_tool_calls", "previous_response_id", "thinking", "store", + "caching", "stream", "temperature", "text", "tool_choice", "tools", + "top_p", "max_tool_calls", "context_management", "expire_at", + "extra_headers", "extra_query", "extra_body", "timeout", "reasoning", + "session", "service_tier", + } + ) + _ARK_RESPONSES_RETRIEVE_KEYS = frozenset( + {"extra_headers", "extra_query", "extra_body", "timeout"} + ) + _ARK_RESPONSES_INPUT_ITEMS_KEYS = frozenset( + { + "after", "before", "include", "limit", "order", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS = frozenset( + { + "input", "model", "encoding_format", "dimensions", "instructions", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_CHAT_KEYS = frozenset( + { + "messages", "model", "frequency_penalty", "function_call", + "logit_bias", "logprobs", "max_tokens", "presence_penalty", + "stop", "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "parallel_tool_calls", "service_tier", + "tool_choice", "response_format", "thinking", "max_completion_tokens", + "user", "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_EMBEDDING_KEYS = frozenset( + { + "input", "model", "encoding_format", "user", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_CHAT_ASYNC_KEYS = _ARK_BATCH_CHAT_KEYS + _ARK_CONTEXT_CREATE_KEYS = frozenset( + { + "model", "messages", "ttl", "mode", "truncation_strategy", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_CONTEXT_COMPLETION_KEYS = frozenset( + { + "context_id", "messages", "model", "frequency_penalty", + "function_call", "logit_bias", "logprobs", "max_tokens", + "presence_penalty", "stop", "stream", "stream_options", + "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "tool_choice", "response_format", + "user", "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_CLASSIFICATION_KEYS = frozenset( + {"query", "model", "labels", "user", "extra_headers", "extra_query", "extra_body", "timeout"} + ) + _ARK_CONTENT_GENERATION_CREATE_KEYS = frozenset( + { + "model", "content", "safety_identifier", "callback_url", + "return_last_frame", "service_tier", "execution_expires_after", + "priority", "generate_audio", "draft", "camera_fixed", "watermark", + "seed", "resolution", "ratio", "duration", "frames", "tools", + "output_format", "omni_reference_task_type", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_CONTENT_GENERATION_LIST_KEYS = frozenset( + { + "page_num", "page_size", "status", "task_ids", "model", "service_tier", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_FILE_CREATE_KEYS = frozenset( + { + "expire_at", "preprocess_configs", "url", "tos", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_FILE_LIST_KEYS = frozenset( + { + "after", "limit", "order", "purpose", "extra_headers", "extra_query", + "extra_body", "timeout", + } + ) + _ARK_IMAGE_GENERATE_KEYS = frozenset( + { + "model", "prompt", "image", "response_format", "size", "seed", + "guidance_scale", "watermark", "optimize_prompt", + "optimize_prompt_options", "extra_headers", "extra_query", "extra_body", + "timeout", "sequential_image_generation", + "sequential_image_generation_options", "tools", "output_format", + "layer_decomposition", "stream", + } + ) + _ARK_BETA_CHAT_KEYS = frozenset( + { + "messages", "model", "response_format", "frequency_penalty", "logit_bias", + "logprobs", "max_tokens", "n", "parallel_tool_calls", "presence_penalty", + "service_tier", "stop", "stream_options", "temperature", "tool_choice", + "tools", "top_logprobs", "top_p", "user", "reasoning_effort", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BOT_CHAT_KEYS = frozenset( + { + "messages", "model", "frequency_penalty", "function_call", "logit_bias", + "logprobs", "max_tokens", "presence_penalty", "stop", "stream", + "stream_options", "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "parallel_tool_calls", "service_tier", + "tool_choice", "response_format", "user", "metadata", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) - def __init__(self, model_name: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "embedding", "responses", "files", "batches", "token_count", + "images", "multimodal_embedding", "context", "content_generation", + "beta_chat", "bot_chat", "classification", + } + ) + + def __init__(self, model_name: str, protocol: str = "ark", options: dict[str, Any] | None = None): + normalized = str(protocol).strip().lower().replace("-", "_") + if normalized in {"ark", "chat", "chat_completion", "chat_completions"}: + self._protocol = "chat_completions" + elif normalized in {"response", "responses"}: + self._protocol = "responses" + else: + raise ValueError("Volcengine 仅支持 ark/chat_completions 或 responses 协议。") self._model_name = model_name + self._options = validate_secret_free_options(options, "Volcengine") + self._options = self._normalize_client_options(self._options) + self._server_verified_protocols = self._normalize_verified_protocols( + self._options.get("server_verified_protocols", ()) + ) settings = get_settings() - self._api_key = settings.volc_access_key # 映射为 ak/sk 或直接使用 api_key (Ark 支持 API Key) - # 注意:Ark SDK 推荐使用 API Key 模式,对应 volc_access_key - if not self._api_key: - logger.error("火山引擎 API Key (VOLC_ACCESS_KEY) 未设置。") - raise ValueError("VOLC_ACCESS_KEY is required for VolcengineProvider") - - self._client: Optional[Ark] = None - self._aclient: Optional[AsyncArk] = None - logger.info(f"初始化 VolcengineProvider (Ark),模型: {model_name}") - - def _get_client(self) -> Ark: + self._api_key = settings.volc_access_key + self._secret_key = getattr(settings, "volc_secret_key", None) + self._ark_api_key = getattr(settings, "ark_api_key", None) + self._base_url = getattr(settings, "volc_base_url", None) + has_ak_sk = bool(self._api_key and self._secret_key) + has_partial_ak_sk = bool(self._api_key or self._secret_key) + if not has_ak_sk and not self._ark_api_key: + if has_partial_ak_sk: + raise ValueError( + "VolcengineProvider 需要同时配置 VOLC_ACCESS_KEY 和 " + "VOLC_SECRET_KEY,或改用 ARK_API_KEY。" + ) + raise ValueError( + "VolcengineProvider 需要配置 ARK_API_KEY,或同时配置 " + "VOLC_ACCESS_KEY 和 VOLC_SECRET_KEY。" + ) + self._client: Any | None = None + self._aclient: Any | None = None + + @property + def resources(self) -> ArkResources: + return ArkResources(self) + + @property + def async_resources(self) -> AsyncArkResources: + return AsyncArkResources(self) + + def _client_options(self) -> dict[str, Any]: + options = self._normalize_client_options( + getattr(self, "_options", {}) or {} + ) + result: dict[str, Any] = {"base_url": self._base_url} if self._base_url else {} + if self._api_key and self._secret_key: + result.update({"ak": self._api_key, "sk": self._secret_key}) + elif getattr(self, "_ark_api_key", None): + result["api_key"] = self._ark_api_key + else: + raise ValueError( + "VolcengineProvider 认证配置不完整:需要 ARK_API_KEY,或同时提供 " + "VOLC_ACCESS_KEY 和 VOLC_SECRET_KEY。" + ) + for key in ("timeout", "max_retries", "region"): + if key in options: + result[key] = options[key] + return result + + @staticmethod + def _normalize_client_options(options: Mapping[str, Any]) -> dict[str, Any]: + """规范化 Ark 客户端级 timeout、重试次数和 region 参数。""" + normalized = dict(options) + if "timeout" in normalized: + value = normalized["timeout"] + if isinstance(value, bool): + raise ValueError("Volcengine options.timeout 必须是正数。") + if isinstance(value, str): + try: + value = float(value.strip()) + except ValueError as exc: + raise ValueError("Volcengine options.timeout 必须是正数。") from exc + if not isinstance(value, (int, float)) or not math.isfinite(float(value)): + # httpx.Timeout is accepted by the Ark SDK but cannot be + # represented in TOML. Preserve such explicit SDK objects. + if not hasattr(value, "connect"): + raise ValueError("Volcengine options.timeout 必须是正数或 httpx.Timeout。") + elif float(value) <= 0: + raise ValueError("Volcengine options.timeout 必须是正数。") + else: + normalized["timeout"] = float(value) + + if "max_retries" in normalized: + value = normalized["max_retries"] + if isinstance(value, bool): + raise ValueError("Volcengine options.max_retries 必须是大于等于 0 的整数。") + if isinstance(value, str): + text = value.strip() + try: + parsed = int(text) + except ValueError as exc: + raise ValueError( + "Volcengine options.max_retries 必须是大于等于 0 的整数。" + ) from exc + if text not in {str(parsed), f"+{parsed}"}: + raise ValueError( + "Volcengine options.max_retries 必须是大于等于 0 的整数。" + ) + value = parsed + elif isinstance(value, float) and math.isfinite(value) and value.is_integer(): + value = int(value) + if not isinstance(value, int) or value < 0: + raise ValueError("Volcengine options.max_retries 必须是大于等于 0 的整数。") + normalized["max_retries"] = value + + if "region" in normalized: + value = normalized["region"] + if not isinstance(value, str) or not value.strip(): + raise ValueError("Volcengine options.region 必须是非空字符串。") + normalized["region"] = value.strip() + return normalized + + @classmethod + def _normalize_verified_protocols(cls, configured: Any) -> frozenset[str]: + """规范化 Ark 渠道的服务端协议登记。""" + if configured is None: + return frozenset() + if isinstance(configured, str): + configured = (configured,) + elif not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError("server_verified_protocols 必须是字符串或协议序列。") + aliases = { + "chat": "chat_completions", + "completion": "chat_completions", + "chat_completion": "chat_completions", + "response": "responses", + } + supported = {"ark", "chat_completions", "responses"} + normalized: set[str] = set() + for value in configured: + if not isinstance(value, str) or not value.strip(): + raise ValueError( + "server_verified_protocols 中的协议必须是非空字符串。" + ) + protocol = value.strip().lower().replace("-", "_") + protocol = aliases.get(protocol, protocol) + if protocol not in supported: + raise ValueError( + f"Volcengine 不支持登记服务端协议: {value}。" + "可选值: ark, chat_completions, responses" + ) + normalized.add(protocol) + return frozenset(normalized) + + def _require_responses_resource(self, operation: str) -> None: + """阻止未验证的 Ark Responses 请求到达远端。""" + if self._protocol != "responses": + raise ValueError(f"Ark {operation} 仅适用于 responses 协议。") + verified = getattr(self, "_server_verified_protocols", None) + if verified is None: + options = getattr(self, "_options", {}) or {} + verified = self._normalize_verified_protocols( + options.get("server_verified_protocols", ()) + ) + if "responses" not in verified: + raise ValueError( + f"Ark 未验证 Responses {operation} 的服务端能力;" + "请在 provider options 中显式配置 " + "server_verified_protocols=['responses']。" + ) + + def _get_client(self) -> Any: if self._client is None: - self._client = Ark(api_key=self._api_key) + ark_class, _ = _load_ark_clients() + self._client = ark_class(**self._client_options()) return self._client - def _get_aclient(self) -> AsyncArk: + def _get_aclient(self) -> Any: if self._aclient is None: - self._aclient = AsyncArk(api_key=self._api_key) + _, async_ark_class = _load_ark_clients() + self._aclient = async_ark_class(**self._client_options()) return self._aclient + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容 Ark 异步客户端的协程和同步分页器返回值。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _merge_options(request: dict[str, Any], options: dict[str, Any]) -> None: + reserved = {"model", "messages", "input", "stream", "instructions", "tools"} + overlap = sorted(reserved.intersection(options)) + if overlap: + raise ValueError(f"Provider options 不允许覆盖请求字段: {', '.join(overlap)}") + for key, value in options.items(): + request.setdefault(key, value) + + @staticmethod + def _validate_model_options( + options: Mapping[str, Any], + allowed: frozenset[str], + endpoint: str, + ) -> None: + unknown = {key: value for key, value in options.items() if key not in allowed} + reject_unsupported_kwargs(endpoint, unknown) + + def _request_options(self) -> dict[str, Any]: + """返回不会误传给 Ark 请求的模型级选项。""" + validate_secret_free_options(getattr(self, "_options", {}) or {}, "Volcengine") + return { + key: value + for key, value in self._options.items() + if key not in { + "timeout", "max_retries", "region", + "server_verified_protocols", + } + } + + @staticmethod + def _validated_extra_body( + value: Any, + endpoint: str, + reserved: set[str], + ) -> dict[str, Any]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{endpoint} extra_body 必须是对象。") + value = validate_secret_free_payload(value, endpoint, "extra_body") + overlap = sorted(set(value).intersection(reserved)) + if overlap: + raise ValueError( + f"{endpoint} extra_body 不允许覆盖请求字段: {', '.join(overlap)}" + ) + return dict(value) + + @staticmethod + def _merge_extra_body( + configured: Mapping[str, Any] | None, + extensions: Mapping[str, Any] | None, + endpoint: str, + ) -> dict[str, Any]: + """合并模型级扩展,拒绝同一字段的静默覆盖。""" + configured_values = dict(configured or {}) + extension_values = dict(extensions or {}) + overlap = sorted(set(configured_values).intersection(extension_values)) + if overlap: + raise ValueError( + f"{endpoint} 模型 options 的扩展字段重复: {', '.join(overlap)}" + ) + return {**configured_values, **extension_values} + + @staticmethod + def _convert_responses_format(response_format: dict[str, Any]) -> dict[str, Any]: + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + if not isinstance(schema, dict): + raise ValueError("Ark Responses 的 json_schema 必须是对象。") + return { + "format": { + "type": "json_schema", + "json_schema": { + "name": schema.get("name", "response"), + "schema": schema.get("schema", response_format.get("schema", {})), + "strict": schema.get("strict", True), + }, + } + } + if response_format.get("type") == "json_object": + return {"format": {"type": "json_object"}} + if "format" in response_format: + return response_format + raise ValueError("Ark Responses 的 response_format 仅支持 json_schema 或 json_object。") + + @staticmethod + def _convert_responses_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + """将统一的 Chat 工具 schema 转换为 Ark Responses 工具 schema。""" + if not tools: + return None + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Ark Responses 工具定义[{index}] 必须是对象。") + if "function" not in tool: + native_tool = dict(tool) + tool_type = native_tool.get("type") + if not isinstance(tool_type, str) or not tool_type.strip(): + raise ValueError( + f"Ark Responses 工具定义[{index}] 缺少有效 type。" + ) + if tool_type == "function": + name = native_tool.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Ark Responses 工具定义[{index}] 缺少 name。" + ) + parameters = native_tool.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].parameters 必须是对象。" + ) + native_tool["parameters"] = dict(parameters) + converted.append(native_tool) + continue + function = tool["function"] + if not isinstance(function, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Ark Responses 工具定义缺少 function.name。") + parameters = function.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].function.parameters 必须是对象。" + ) + item: dict[str, Any] = { + "type": "function", + "name": name, + "parameters": dict(parameters), + } + if function.get("description") is not None: + item["description"] = function["description"] + if "strict" in function or "strict" in tool: + item["strict"] = function.get("strict", tool.get("strict")) + converted.append(item) + return converted + + @staticmethod + def _convert_responses_tool_choice(tool_choice: Any) -> Any: + """将 OpenAI 风格的工具选择转换为 Ark Responses schema。""" + if tool_choice is None: + return None + if isinstance(tool_choice, str): + aliases = {"required": "required", "auto": "auto", "none": "none"} + normalized = tool_choice.lower() + if normalized in aliases: + return aliases[normalized] + raise ValueError(f"Ark Responses 不支持 tool_choice: {tool_choice}") + if isinstance(tool_choice, dict): + if tool_choice.get("type") == "function": + function = tool_choice.get("function", tool_choice) + if not isinstance(function, Mapping): + raise ValueError("Ark Responses 的 function tool_choice 必须是对象。") + name = function.get("name") + if not name: + raise ValueError("Ark Responses 的 function tool_choice 缺少 name。") + return {"type": "function", "name": name} + if tool_choice.get("type") in { + "mcp", "web_search", "knowledge_search" + }: + return dict(tool_choice) + raise ValueError("Ark Responses 的 tool_choice 必须是 auto、none、required 或工具对象。") + + def _build_chat_request(self, prompt: str | None = None, system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, temperature: float | None = None, + stream: bool = True, *, messages: Any = None, max_tokens: int | None = None, + top_p: float | None = None, top_k: int | None = None, seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, user: str | None = None, + extra_headers: dict[str, str] | None = None, extra_query: dict[str, Any] | None = None, + frequency_penalty: float | None = None, presence_penalty: float | None = None, + repetition_penalty: float | None = None, n: int | None = None, + logit_bias: dict[str, int] | None = None, logprobs: bool | None = None, + top_logprobs: int | None = None, parallel_tool_calls: bool | None = None, + service_tier: str | None = None, thinking: dict[str, Any] | None = None, + reasoning: dict[str, Any] | None = None, stream_options: dict[str, Any] | None = None, + timeout: float | None = None, **ignored: Any) -> dict[str, Any]: + reject_unsupported_kwargs("Ark Chat Completions", ignored) + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Ark Chat Completions", + ) + request: dict[str, Any] = {"model": self._model_name, "messages": normalize_messages(prompt, system_prompt, messages), "stream": stream} + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._ARK_CHAT_MODEL_OPTION_KEYS, + "Ark Chat Completions", + ) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Chat Completions", + {"model", "messages", "input", "stream"}, + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + if configured_max_tokens is not None and configured_max_completion_tokens is not None: + raise ValueError( + "Ark Chat Completions options 不能同时设置 max_tokens 和 " + "max_completion_tokens。" + ) + configured_top_k = configured_options.pop("top_k", None) + configured_seed = configured_options.pop("seed", None) + configured_scalar_extra_keys: set[str] = set() + if configured_top_k is not None: + if "top_k" in configured_extra_body: + raise ValueError( + "Ark Chat Completions 模型 options 的 top_k 与 extra_body 重复。" + ) + configured_extra_body["top_k"] = configured_top_k + configured_scalar_extra_keys.add("top_k") + if configured_seed is not None: + if "seed" in configured_extra_body: + raise ValueError( + "Ark Chat Completions 模型 options 的 seed 与 extra_body 重复。" + ) + configured_extra_body["seed"] = configured_seed + configured_scalar_extra_keys.add("seed") + self._merge_options(request, configured_options) + if temperature is None and "temperature" not in request: + request["temperature"] = 0.7 + if max_tokens is None: + if configured_max_tokens is not None: + request["max_tokens"] = configured_max_tokens + elif configured_max_completion_tokens is not None: + request["max_completion_tokens"] = configured_max_completion_tokens + if configured_extra_body: + request["extra_body"] = dict(configured_extra_body) + if temperature is not None: + request["temperature"] = temperature + if tools: + request["tools"] = tools + if max_tokens is not None: + request["max_tokens"] = max_tokens + if top_p is not None: + request["top_p"] = top_p + for key, value in { + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + "repetition_penalty": repetition_penalty, + "n": n, + "logit_bias": logit_bias, + "logprobs": logprobs, + "top_logprobs": top_logprobs, + "parallel_tool_calls": parallel_tool_calls, + "service_tier": service_tier, + "thinking": thinking, + "reasoning_effort": (reasoning or {}).get("effort") if isinstance(reasoning, dict) else None, + "stream_options": stream_options, + "timeout": timeout, + "extra_headers": extra_headers, + "extra_query": extra_query, + }.items(): + if value is not None: + request[key] = value + if top_k is not None: + if "top_k" in configured_extra_body and "top_k" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Chat Completions top_k 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_extra_body and "seed" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Chat Completions seed 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["seed"] = seed + if stop is not None: + request["stop"] = stop + if response_format is not None: + request["response_format"] = response_format + if tool_choice is not None: + request["tool_choice"] = tool_choice + if user is not None: + request["user"] = user + request_extra_body = self._validated_extra_body( + extra_body, + "Ark Chat Completions", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Ark Chat Completions extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Ark Chat Completions", + set(request).difference({"extra_body"}), + ) + return request + + def _build_responses_request(self, request: CompletionRequest) -> dict[str, Any]: + validate_secret_free_request_overrides( + getattr(request, "extra_headers", None), + getattr(request, "extra_query", None), + "Ark Responses", + ) + unsupported_fields = ( + "repetition_penalty", "n", "logit_bias", "logprobs", "modalities", "audio", + "prediction", "web_search_options", "stop", "include", "background", + "metadata", "user", "moderation", "prompt_cache_options", "top_logprobs", + "safety_identifier", "prompt_cache_key", "prompt_cache_retention", + "truncation", "stream_options", "cache_control", "container", + "inference_geo", "mcp_servers", "output_config", "output_format", + "speed", "betas", "diagnostics", "fallback_credit_token", "fallbacks", + ) + reject_unsupported_kwargs( + "Ark Responses", + { + key: getattr(request, key) + for key in unsupported_fields + if getattr(request, key, None) is not None + }, + ) + normalized_input = normalize_responses_input( + request.prompt, + request.system_prompt, + request.messages, + provider="ark", + ) + params: dict[str, Any] = { + "model": self._model_name, + "input": normalized_input, + "stream": request.stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._ARK_RESPONSES_MODEL_OPTION_KEYS, + "Ark Responses", + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + configured_max_output_tokens = configured_options.pop("max_output_tokens", None) + configured_limits = { + key: value + for key, value in { + "max_output_tokens": configured_max_output_tokens, + "max_tokens": configured_max_tokens, + "max_completion_tokens": configured_max_completion_tokens, + }.items() + if value is not None + } + if len(configured_limits) > 1: + raise ValueError( + "Ark Responses options 不能同时设置多个输出长度字段: " + + ", ".join(configured_limits) + ) + configured_response_format = configured_options.pop("response_format", None) + configured_reasoning_effort = configured_options.pop("reasoning_effort", None) + configured_frequency_penalty = configured_options.pop("frequency_penalty", None) + configured_presence_penalty = configured_options.pop("presence_penalty", None) + configured_tool_choice = configured_options.pop("tool_choice", None) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Responses", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_top_k = configured_options.pop("top_k", None) + configured_seed = configured_options.pop("seed", None) + configured_conversation = configured_options.pop("conversation", None) + configured_session = configured_options.pop("session", None) + if configured_conversation is not None and configured_session is not None: + raise ValueError( + "Ark Responses 模型 options 不能同时设置 conversation 和 session。" + ) + configured_scalar_extra_keys: set[str] = set() + if configured_top_k is not None: + if "top_k" in configured_extra_body: + raise ValueError( + "Ark Responses 模型 options 的 top_k 与 extra_body 重复。" + ) + configured_extra_body["top_k"] = configured_top_k + configured_scalar_extra_keys.add("top_k") + if configured_seed is not None: + if "seed" in configured_extra_body: + raise ValueError( + "Ark Responses 模型 options 的 seed 与 extra_body 重复。" + ) + configured_extra_body["seed"] = configured_seed + configured_scalar_extra_keys.add("seed") + if request.system_prompt and request.messages is None: + params["instructions"] = request.system_prompt + if getattr(request, "_temperature_explicit", True) and request.temperature is not None: + params["temperature"] = request.temperature + if request.max_tokens is not None: + params["max_output_tokens"] = request.max_tokens + elif configured_limits: + params["max_output_tokens"] = next(iter(configured_limits.values())) + if request.top_p is not None: + params["top_p"] = request.top_p + sampling_extensions = { + "frequency_penalty": ( + request.frequency_penalty + if request.frequency_penalty is not None + else configured_frequency_penalty + ), + "presence_penalty": ( + request.presence_penalty + if request.presence_penalty is not None + else configured_presence_penalty + ), + } + for key, value in sampling_extensions.items(): + if value is None: + continue + if key in configured_extra_body: + raise ValueError(f"Ark Responses {key} 与模型 options.extra_body 重复。") + configured_extra_body[key] = value + reject_unsupported_kwargs("Ark Responses", {"top_logprobs": request.top_logprobs}) + if request.top_k is not None: + if "top_k" in configured_extra_body and "top_k" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Responses top_k 与模型 options.extra_body 重复。" + ) + configured_extra_body["top_k"] = request.top_k + if request.seed is not None: + if "seed" in configured_extra_body and "seed" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Responses seed 与模型 options.extra_body 重复。" + ) + configured_extra_body["seed"] = request.seed + converted_tools = self._convert_responses_tools(request.tools) + if converted_tools: + params["tools"] = converted_tools + effective_response_format = request.response_format or configured_response_format + if effective_response_format: + params["text"] = self._convert_responses_format(effective_response_format) + if request.tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice(request.tool_choice) + elif configured_tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice( + configured_tool_choice + ) + for key in ("previous_response_id", "reasoning", "thinking", "store", "caching", "parallel_tool_calls", "max_tool_calls", "context_management", "expire_at", "service_tier"): + value = getattr(request, key, None) + if value is not None: + params[key] = value + if request.reasoning is None and configured_reasoning_effort is not None: + params["reasoning"] = {"effort": configured_reasoning_effort} + if request.conversation is not None and request.session is not None: + raise ValueError( + "Ark Responses 不能同时传 conversation 和 session。" + ) + # Ark calls the response conversation state a session. Keep + # `conversation` as the portable request field for OpenAI-compatible + # channels while mapping it to the SDK's actual parameter here. + request_session = request.session + request_conversation = request.conversation + if request_session is not None: + if not isinstance(request_session, Mapping): + raise ValueError("Ark Responses session 必须是对象。") + params["session"] = dict(request_session) + elif request_conversation is not None: + if not isinstance(request_conversation, Mapping): + raise ValueError("Ark Responses conversation 必须是对象。") + params["session"] = dict(request_conversation) + elif configured_session is not None: + if not isinstance(configured_session, Mapping): + raise ValueError("Ark Responses 模型 options.session 必须是对象。") + params["session"] = dict(configured_session) + elif configured_conversation is not None: + if not isinstance(configured_conversation, Mapping): + raise ValueError("Ark Responses 模型 options.conversation 必须是对象。") + params["session"] = dict(configured_conversation) + if request.timeout is not None: + params["timeout"] = request.timeout + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + self._merge_options(params, configured_options) + if configured_extra_body: + params["extra_body"] = dict(configured_extra_body) + request_extra_body = self._validated_extra_body( + request.extra_body, + "Ark Responses", + set(params).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(params.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Ark Responses extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + params.setdefault("extra_body", {}).update(request_extra_body) + if params.get("extra_body"): + params["extra_body"] = self._validated_extra_body( + params["extra_body"], + "Ark Responses", + set(params).difference({"extra_body"}), + ) + return params + + @classmethod + def _validate_native_response_kwargs(cls, kwargs: Mapping[str, Any]) -> dict[str, Any]: + """校验 Ark SDK 原生 Responses 参数,不改变其必填字段语义。""" + params = dict(kwargs) + unsupported = { + key: value + for key, value in params.items() + if key not in cls._ARK_RESPONSES_CREATE_KEYS + } + reject_unsupported_kwargs("Ark Responses", unsupported) + if "model" not in params or not isinstance(params.get("model"), str) or not params["model"].strip(): + raise ValueError("Ark Responses 原生调用必须显式提供 model。") + if "input" not in params or params.get("input") is None: + raise ValueError("Ark Responses 创建需要 input。") + headers, query = validate_secret_free_request_overrides( + params.get("extra_headers"), + params.get("extra_query"), + "Ark Responses", + ) + if "extra_headers" in params: + params["extra_headers"] = headers + if "extra_query" in params: + params["extra_query"] = query + if "extra_body" in params and params["extra_body"] is not None: + params["extra_body"] = validate_secret_free_payload( + params["extra_body"], "Ark Responses", "extra_body" + ) + if "session" in params and params["session"] is not None: + if not isinstance(params["session"], Mapping): + raise ValueError("Ark Responses session 必须是对象。") + params["session"] = dict(params["session"]) + return params + + def _embedding_options(self) -> dict[str, Any]: + """返回 Ark Embeddings 接受的模型级参数,排除聊天选项。""" + supported = {"dimensions", "encoding_format", "user", "extra_body"} + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _validate_resource_kwargs( + resource: str, + kwargs: dict[str, Any], + supported: Collection[str], + ) -> None: + validate_secret_free_request_overrides( + kwargs.get("extra_headers"), + kwargs.get("extra_query"), + f"Ark {resource}", + ) + if "extra_body" in kwargs: + validate_secret_free_payload( + kwargs.get("extra_body"), + f"Ark {resource}", + "extra_body", + ) + unsupported = sorted(key for key in kwargs if key not in supported) + if unsupported: + raise ValueError(f"Ark {resource} 不支持请求参数: {', '.join(unsupported)}") + + @staticmethod + def _validate_file_wait_args( + file_id: str, + poll_interval: float | None, + max_wait_seconds: float | None, + ) -> tuple[str, float | None, float | None]: + """校验 Ark 文件轮询参数,避免无界等待或底层类型错误。""" + file_id = VolcengineProvider._require_non_empty_string( + file_id, + "Ark 文件等待的 file_id", + ) + + def positive_finite(value: float | None, name: str) -> float | None: + if value is None: + return None + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(float(value)) + or float(value) <= 0 + ): + raise ValueError(f"Ark 文件等待的 {name} 必须是有限正数。") + return float(value) + + return ( + file_id, + positive_finite(poll_interval, "poll_interval"), + positive_finite(max_wait_seconds, "max_wait_seconds"), + ) + + @staticmethod + def _require_non_empty_string(value: Any, label: str) -> str: + """校验 SDK 资源 ID、模型名等字符串参数。""" + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{label} 必须是非空字符串。") + return value.strip() + + @classmethod + def _prepare_batch_kwargs( + cls, + kwargs: dict[str, Any], + model_name: str, + resource: str, + supported: frozenset[str], + required: tuple[str, ...], + ) -> dict[str, Any]: + params = dict(kwargs) + if params.get("model") is None: + params["model"] = model_name + missing = [ + name + for name in required + if name not in params or params[name] is None + ] + if missing: + raise ValueError( + f"Ark {resource} 缺少必填参数: {', '.join(missing)}" + ) + model = params.get("model") + if not isinstance(model, str) or not model.strip(): + raise ValueError(f"Ark {resource} 的 model 必须是非空字符串。") + cls._validate_resource_kwargs(resource, params, supported) + return params + + @staticmethod + def _extract_result(response: Any) -> CompletionResult: + choices = field(response, "choices", []) or [] + text = "" + calls: list[dict[str, Any]] = [] + if choices: + message = field(choices[0], "message") + text = content_to_text(field(message, "content", "")) + for call in field(message, "tool_calls", []) or []: + fn = field(call, "function") + calls.append({ + "id": field(call, "id"), + "name": field(fn, "name"), + "arguments": normalize_tool_arguments(field(fn, "arguments", "")), + }) + output_text = field(response, "output_text") + if isinstance(output_text, str): + text = output_text + for item in field(response, "output", []) or []: + item_type = field(item, "type") + if item_type in {"function_call", "custom_tool_call"}: + calls.append( + { + "id": field(item, "call_id") or field(item, "id"), + "type": item_type, + "name": field(item, "name"), + "arguments": normalize_tool_arguments( + field(item, "arguments", field(item, "input", "")) + ), + } + ) + usage_dict = normalize_usage(field(response, "usage")) + reasoning = "" + refusal: str | None = None + if choices: + message = field(choices[0], "message") + for name in ("reasoning_content", "reasoning"): + value = field(message, name) + if isinstance(value, str): + reasoning += value + candidate_refusal = field(message, "refusal") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + for item in field(response, "output", []) or []: + if field(item, "type") in {"reasoning", "reasoning_item"}: + summary = field(item, "summary") + if isinstance(summary, str): + reasoning += summary + elif isinstance(summary, (list, tuple)): + for entry in summary: + reasoning_text = field(entry, "text") + if isinstance(reasoning_text, str): + reasoning += reasoning_text + for content in field(item, "content", []) or []: + reasoning_text = field(content, "text") + if isinstance(reasoning_text, str): + reasoning += reasoning_text + fallback = field(item, "text") + if isinstance(fallback, str): + reasoning += fallback + if field(item, "type") == "refusal": + candidate_refusal = field(item, "refusal") or field(item, "text") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + for content in field(item, "content", []) or []: + if field(content, "type") != "refusal": + continue + candidate_refusal = field(content, "refusal") or field(content, "text") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + return CompletionResult(text=text, tool_calls=calls, usage=usage_dict, + finish_reason=field(choices[0], "finish_reason") if choices else field(response, "status"), + response_id=field(response, "id"), refusal=refusal, + reasoning=reasoning, raw=response) + + @staticmethod + def _raise_for_response_error(response: Any) -> None: + status = field(response, "status") + if status == "failed": + error = field(response, "error") + message = error if isinstance(error, str) else field(error, "message", "Ark Responses 请求失败") + raise RuntimeError(str(message)) + if status == "incomplete": + details = field(response, "incomplete_details") + reason = field(details, "reason", "unknown") + raise RuntimeError(f"Ark Responses 响应不完整: {reason}") + if status == "cancelled": + raise RuntimeError("Ark Responses 请求已取消") + + @staticmethod + def _chat_stream_events(chunk: Any) -> list[StreamEvent]: + raise_for_stream_error_event(chunk, "Ark Chat Completions") + OpenAICompatibleProvider._raise_for_chat_response_error( + chunk, + allow_empty_choices=True, + provider="Ark Chat Completions", + ) + choices = field(chunk, "choices", []) or [] + usage = normalize_usage(field(chunk, "usage")) + if not choices: + return [StreamEvent(type="usage", usage=usage, raw=chunk)] if usage else [] + choice = choices[0] + delta = field(choice, "delta") + text = content_to_text(field(delta, "content", "")) + reasoning = field(delta, "reasoning_content") or field(delta, "reasoning") or "" + refusal = field(delta, "refusal") + calls = field(delta, "tool_calls", []) or [] + finish_reason = field(choice, "finish_reason") + events: list[StreamEvent] = [] + if isinstance(refusal, str) and refusal: + events.append(StreamEvent(type="refusal_delta", refusal=refusal, raw=chunk)) + response_id = field(chunk, "id") + if text: + events.append( + StreamEvent(type="text_delta", text=text, response_id=response_id, raw=chunk) + ) + if isinstance(reasoning, str) and reasoning: + events.append( + StreamEvent(type="reasoning_delta", reasoning=reasoning, response_id=response_id, raw=chunk) + ) + if calls: + for position, call in enumerate(calls): + function = field(call, "function") + events.append( + StreamEvent( + type="tool_call_delta", + tool_call={ + "id": field(call, "id"), + "index": field(call, "index", position), + "type": field(call, "type", "function"), + "name": field(function, "name"), + "arguments": field(function, "arguments", ""), + }, + response_id=response_id, + raw=chunk, + ) + ) + if finish_reason: + events.append( + StreamEvent( + type="finish", + usage=usage, + finish_reason=finish_reason, + response_id=field(chunk, "id"), + raw=chunk, + ) + ) + elif usage: + if events: + events[-1].usage = usage + else: + events.append(StreamEvent(type="usage", usage=usage, raw=chunk)) + return events + + @staticmethod + def _chat_stream_event(chunk: Any) -> StreamEvent | None: + events = VolcengineProvider._chat_stream_events(chunk) + return events[0] if events else None + + @staticmethod + def _merge_response_tool_call( + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + ) -> tuple[Any, dict[str, Any]]: + index = tool_call.get("index") + call_id = tool_call.get("call_id") or tool_call.get("id") + key: Any = ("index", index) if index is not None else ("id", call_id) + if key == ("id", None): + key = ("position", len(accumulator)) + merged = accumulator.setdefault(key, {}) + for name in ("id", "call_id", "index", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + arguments = tool_call.get("arguments") + if arguments is not None and arguments != "": + merged["arguments"] = normalize_tool_arguments(arguments) + merged.setdefault("type", "function_call") + merged.setdefault("arguments", "") + return key, merged + + @classmethod + def _responses_completion_event( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + ) -> tuple[Any, StreamEvent] | None: + if converted.type != "tool_call_completed" or not converted.tool_call: + return None + key, merged = cls._merge_response_tool_call(accumulator, converted.tool_call) + validate_complete_tool_call(merged, "Ark Responses") + return key, StreamEvent( + type="tool_call_completed", + tool_call=dict(merged), + response_id=converted.response_id, + raw=converted.raw, + ) + + def _responses_stream_event( + self, + event: Any, + tool_metadata: dict[Any, dict[str, Any]] | None = None, + ) -> StreamEvent | None: + """复用 Responses 事件转换,确保 Ark 与 OpenAI 的工具关联一致。""" + return OpenAICompatibleProvider._responses_stream_event( + event, + tool_metadata, + error_provider="Ark Responses", + response_error_handler=self._raise_for_response_error, + ) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Ark stream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("stream_events") + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + if terminal_seen: + continue + event_type = field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "Ark Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep a response-body generator open + # after the terminal event; breaking here can surface + # noisy generator-close errors and leak a connection. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Ark Responses 流在 response.completed 之前结束。") + return + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + if converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_chat_tool_calls: + validate_complete_tool_call(tool_call, "Ark Chat Completions") + completed_chat_tool_calls.add(key) + yield StreamEvent(type="tool_call_completed", tool_call=dict(tool_call), response_id=converted.response_id, raw=converted.raw) + yield converted + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 流在 finish 事件之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Ark astream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("astream_events") + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + if terminal_seen: + continue + event_type = field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "Ark Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep the response body generator + # open after the terminal event. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Ark Responses 异步流在 response.completed 之前结束。") + return + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + if converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_chat_tool_calls: + validate_complete_tool_call(tool_call, "Ark Chat Completions") + completed_chat_tool_calls.add(key) + yield StreamEvent(type="tool_call_completed", tool_call=dict(tool_call), response_id=converted.response_id, raw=converted.raw) + yield converted + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 异步流在 finish 事件之前结束。") + @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - def invoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> Generator[str, None, None]: - """同步调用火山引擎 LLM (CSE Sensor)。""" - logger.info(f"调用火山引擎 LLM ({self._model_name}),流式: {stream}") - client = self._get_client() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Ark complete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("complete") + response = self._get_client().responses.create( + **self._build_responses_request(request) + ) + self._raise_for_response_error(response) + else: + response = self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncArk 聚合完整结果,保留 Responses/Chat 元数据。""" + request = coerce_completion_request(request, kwargs, "Ark acomplete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("acomplete") + response = await retry_async_call( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + else: + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + return self._extract_result(response) + + def invoke(self, prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", + tools: list[dict[str, Any]] | None = None, stream: bool = True, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, max_tokens: int | None = None, top_p: float | None = None, + stop: str | list[str] | None = None, response_format: dict[str, Any] | None = None, + tool_choice: Any = None, extra_body: dict[str, Any] | None = None, **kwargs: Any) -> Generator[str, None, None]: + request = CompletionRequest(prompt=prompt, system_prompt=system_prompt, messages=messages, tools=tools, stream=stream, temperature=temperature, max_tokens=max_tokens, top_p=top_p, stop=stop, response_format=response_format, tool_choice=tool_choice, extra_body=extra_body, **kwargs) + if self._protocol == "responses": + self._require_responses_resource("invoke") + if tools: + raise ValueError( + "Ark Responses invoke 仅返回文本,不支持 tools;" + "请使用 complete 或 stream_events 获取工具调用。" + ) if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + raise_for_stream_error_event(event, "Ark Responses") + converted = self._responses_stream_event(event) + if field(event, "type") == "response.completed": + terminal_seen = True + if converted and converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {converted.refusal}") + if converted and converted.text: + yield converted.text + if not terminal_seen: + raise RuntimeError("Ark Responses 流在 response.completed 之前结束。") + else: + response = retry_sync_call( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {result.refusal}") + if result.text: + yield result.text + return + if stream: + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {event.refusal}") + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 流在 finish 事件之前结束。") + else: + response = retry_sync_call( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {result.refusal}") + if result.text: + yield result.text + + async def ainvoke(self, prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", + tools: list[dict[str, Any]] | None = None, stream: bool = True, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + **kwargs: Any) -> AsyncGenerator[str, None]: + if prompt is not None: + kwargs["prompt"] = prompt + kwargs["system_prompt"] = system_prompt + if tools is not None: + kwargs["tools"] = tools + kwargs["stream"] = stream + kwargs["temperature"] = temperature + request = CompletionRequest(**kwargs) + if self._protocol == "responses": + self._require_responses_resource("ainvoke") + if request.tools: + raise ValueError( + "Ark Responses ainvoke 仅返回文本,不支持 tools;" + "请使用 acomplete 或 astream_events 获取工具调用。" + ) + if request.stream: + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + raise_for_stream_error_event(event, "Ark Responses") + converted = self._responses_stream_event(event) + if field(event, "type") == "response.completed": + terminal_seen = True + if converted and converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {converted.refusal}") + if converted and converted.text: + yield converted.text + if not terminal_seen: + raise RuntimeError("Ark Responses 异步流在 response.completed 之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"火山引擎 LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"火山引擎 LLM ({self._model_name}) 出错: {e}", exc_info=True) - raise + response = await retry_async_call( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {result.refusal}") + if result.text: + yield result.text + return + if request.stream: + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {event.refusal}") + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 异步流在 finish 事件之前结束。") + else: + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {result.refusal}") + if result.text: + yield result.text @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - async def ainvoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: - """异步调用火山引擎 LLM (CSE Sensor)。""" - logger.info(f"异步调用 火山引擎 LLM ({self._model_name}),流式: {stream}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"火山引擎 LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"火山引擎 LLM ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Ark Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + self._validate_resource_kwargs( + "Embedding", + request_options, + {"encoding_format", "dimensions", "user", "extra_body", "extra_headers", "extra_query", "timeout"}, + ) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options).union(request_options), + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, "Ark Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = self._get_client().embeddings.create(**request) + return [normalize_embedding_vector(item.embedding) for item in response.data] @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: - """同步向量化文档。""" - logger.info(f"调用火山引擎嵌入 ({self._model_name}),数量: {len(texts)}") + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Ark Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + self._validate_resource_kwargs( + "Embedding", + request_options, + {"encoding_format", "dimensions", "user", "extra_body", "extra_headers", "extra_query", "timeout"}, + ) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options).union(request_options), + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, "Ark Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = await self._resolve_async_result(self._get_aclient().embeddings.create(**request)) + return [normalize_embedding_vector(item.embedding) for item in response.data] + + def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + purpose = self._require_non_empty_string(purpose, "Ark 文件 purpose") + self._validate_resource_kwargs("文件创建", kwargs, self._ARK_FILE_CREATE_KEYS) + return self._get_client().files.create(file=file, purpose=purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("文件列表", kwargs, self._ARK_FILE_LIST_KEYS) + return self._get_client().files.list(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件获取", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return self._get_client().files.retrieve(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件删除", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return self._get_client().files.delete(file_id, **kwargs) + + def wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """等待 Ark 文件处理完成,仅转发 SDK 的轮询参数。""" + file_id, poll_interval, max_wait_seconds = self._validate_file_wait_args( + file_id, + poll_interval, + max_wait_seconds, + ) + self._validate_resource_kwargs("文件等待", kwargs, set()) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return self._get_client().files.wait_for_processing(id=file_id, **poll_kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + self._validate_resource_kwargs( + "Responses retrieve_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return self._get_client().responses.retrieve(response_id, **kwargs) + + def create_response(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """创建 Ark Responses;可传统一请求或原生 SDK 参数。""" + self._require_responses_resource("create_response") + if request is not None: + if kwargs: + raise ValueError("Ark Responses 不能同时传 request 和原生关键字参数。") + params = self._build_responses_request(request) + else: + params = self._validate_native_response_kwargs(kwargs) + return self._get_client().responses.create(**params) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + self._validate_resource_kwargs( + "Responses delete_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return self._get_client().responses.delete(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + self._validate_resource_kwargs( + "Responses list_response_input_items", + kwargs, + self._ARK_RESPONSES_INPUT_ITEMS_KEYS, + ) + return self._get_client().input_items.list(response_id, **kwargs) + + def create_batch_chat(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Chat", + self._ARK_BATCH_CHAT_KEYS, + ("messages",), + ) + return self._get_client().batch.chat.completions.create(**params) + + def create_batch_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Embedding", + self._ARK_BATCH_EMBEDDING_KEYS, + ("input",), + ) + return self._get_client().batch.embeddings.create(**params) + + def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Multimodal Embedding", + self._ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS, + ("input",), + ) + return self._get_client().batch.multimodal_embeddings.create(**params) + + def create_context(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context", + self._ARK_CONTEXT_CREATE_KEYS, + ("messages",), + ) + return self._get_client().context.create(**params) + + def context_complete(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context Completion", + self._ARK_CONTEXT_COMPLETION_KEYS, + ("context_id", "messages"), + ) + params["context_id"] = self._require_non_empty_string( + params["context_id"], + "Ark Context Completion 的 context_id", + ) + return self._get_client().context.completions.create(**params) + + def create_content_generation_task(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Content Generation", + self._ARK_CONTENT_GENERATION_CREATE_KEYS, + ("content",), + ) + return self._get_client().content_generation.tasks.create(**params) + + def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 获取", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().content_generation.tasks.get(task_id=task_id, **kwargs) + + def list_content_generation_tasks(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Content Generation 列表", + kwargs, + self._ARK_CONTENT_GENERATION_LIST_KEYS, + ) + return self._get_client().content_generation.tasks.list(**kwargs) + + def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 删除", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().content_generation.tasks.delete(task_id=task_id, **kwargs) + + def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + self._validate_resource_kwargs("Tokenization", kwargs, {"user", "extra_headers", "extra_query", "extra_body", "timeout"}) + response = self._get_client().tokenization.create(text=text, model=self._model_name, **kwargs) + values = field(response, "data", []) or [] + if not values: + raise RuntimeError("Ark tokenization 响应缺少 data。") + return sum(int(field(item, "total_tokens", 0)) for item in values) + + def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Multimodal Embedding", + kwargs, + {"encoding_format", "dimensions", "instructions", "sparse_embedding", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().multimodal_embeddings.create(input=inputs, model=self._model_name, **kwargs) + + def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_input_items") + self._validate_resource_kwargs( + "Responses list_input_items", kwargs, self._ARK_RESPONSES_INPUT_ITEMS_KEYS + ) + return self._get_client().input_items.list(response_id, **kwargs) + + def generate_batch_chat(self, **kwargs: Any) -> Any: + """兼容旧名称,转发到唯一的 Batch Chat 实现。""" + return self.create_batch_chat(**kwargs) + + def generate_image(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Images", kwargs, self._ARK_IMAGE_GENERATE_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().images.generate(**kwargs) + + def beta_chat_parse(self, **kwargs: Any) -> Any: + """调用 Ark beta.chat 的结构化解析接口。""" + self._validate_resource_kwargs("Beta Chat Parse", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().beta.chat.completions.parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + """调用 Ark beta.chat 的原生流式上下文管理器。""" + self._validate_resource_kwargs("Beta Chat Stream", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().beta.chat.completions.stream(**kwargs) + + def bot_chat(self, **kwargs: Any) -> Any: + """调用 Ark Bot Chat 原生接口。""" + self._validate_resource_kwargs("Bot Chat", kwargs, self._ARK_BOT_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().bot_chat.completions.create(**kwargs) + + def _get_classification_resource(self) -> Any: + """返回 Ark Classification 资源。 + + Ark SDK 当前版本包含 classification 模块,但客户端构造函数没有将其 + 挂载到顶层。优先使用未来版本可能提供的顶层资源,否则显式实例化 + SDK 官方资源类;导入失败时保留可定位的 NotImplementedError。 + """ client = self._get_client() - start_time = time.perf_counter() - + resource = getattr(client, "classification", None) + if resource is not None: + return resource + resource = getattr(self, "_classification_resource", None) + if resource is not None: + return resource try: - # Ark SDK 嵌入调用 - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"火山引擎嵌入 ({self._model_name}) 完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"火山引擎嵌入 ({self._model_name}) 出错: {e}", exc_info=True) - raise + from volcenginesdkarkruntime.resources.classification import ( # type: ignore[import-untyped] + Classification, + ) + except (ImportError, AttributeError) as exc: + raise NotImplementedError( + "当前 volcengine-python-sdk 未提供 Ark Classification 资源。" + ) from exc + resource = Classification(client) + self._classification_resource = resource + return resource - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: - """异步向量化文档。""" - logger.info(f"异步调用 火山引擎嵌入 ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() - start_time = time.perf_counter() - + def classify( + self, + query: str, + labels: list[str], + model: str | None = None, + **kwargs: Any, + ) -> Any: + """调用 Ark Classification 接口。""" + query = self._require_non_empty_string(query, "Ark Classification 的 query") + if not isinstance(labels, list) or not labels or any( + not isinstance(label, str) or not label.strip() for label in labels + ): + raise ValueError("Ark Classification 的 labels 必须是非空字符串列表。") + labels = [label.strip() for label in labels] + effective_model = ( + self._model_name + if model is None + else self._require_non_empty_string(model, "Ark Classification 的 model") + ) + self._validate_resource_kwargs( + "Classification", + kwargs, + {"user", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_classification_resource().create( + query=query, + model=effective_model, + labels=labels, + **kwargs, + ) + + async def async_upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + purpose = self._require_non_empty_string(purpose, "Ark 文件 purpose") + self._validate_resource_kwargs("文件创建", kwargs, self._ARK_FILE_CREATE_KEYS) + return await self._resolve_async_result(self._get_aclient().files.create(file=file, purpose=purpose, **kwargs)) + + async def async_list_files(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("文件列表", kwargs, self._ARK_FILE_LIST_KEYS) + return await self._resolve_async_result(self._get_aclient().files.list(**kwargs)) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件获取", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return await self._resolve_async_result(self._get_aclient().files.retrieve(file_id, **kwargs)) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件删除", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return await self._resolve_async_result(self._get_aclient().files.delete(file_id, **kwargs)) + + async def async_wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """异步等待 Ark 文件处理完成,仅转发 SDK 的轮询参数。""" + file_id, poll_interval, max_wait_seconds = self._validate_file_wait_args( + file_id, + poll_interval, + max_wait_seconds, + ) + self._validate_resource_kwargs("文件等待", kwargs, set()) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return await self._resolve_async_result( + self._get_aclient().files.wait_for_processing(id=file_id, **poll_kwargs) + ) + + async def async_retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + self._validate_resource_kwargs( + "Responses retrieve_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return await self._resolve_async_result(self._get_aclient().responses.retrieve(response_id, **kwargs)) + + async def async_create_response(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """异步创建 Ark Responses;可传统一请求或原生 SDK 参数。""" + self._require_responses_resource("create_response") + if request is not None: + if kwargs: + raise ValueError("Ark Responses 不能同时传 request 和原生关键字参数。") + params = self._build_responses_request(request) + else: + params = self._validate_native_response_kwargs(kwargs) + return await self._resolve_async_result(self._get_aclient().responses.create(**params)) + + async def async_delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + self._validate_resource_kwargs( + "Responses delete_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return await self._resolve_async_result(self._get_aclient().responses.delete(response_id, **kwargs)) + + async def async_list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + self._validate_resource_kwargs( + "Responses list_response_input_items", + kwargs, + self._ARK_RESPONSES_INPUT_ITEMS_KEYS, + ) + return await self._resolve_async_result( + self._get_aclient().input_items.list(response_id, **kwargs) + ) + + async def async_list_input_items(self, response_id: str, **kwargs: Any) -> Any: + """Ark 顶层 input_items 资源的异步直观别名。""" + self._require_responses_resource("list_input_items") + self._validate_resource_kwargs( + "Responses list_input_items", kwargs, self._ARK_RESPONSES_INPUT_ITEMS_KEYS + ) + return await self._resolve_async_result( + self._get_aclient().input_items.list(response_id, **kwargs) + ) + + async def async_create_batch_chat(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Chat", + self._ARK_BATCH_CHAT_ASYNC_KEYS, + ("messages",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.chat.completions.create(**params) + ) + + async def async_generate_batch_chat(self, **kwargs: Any) -> Any: + """兼容旧名称,转发到当前 Ark Batch Chat 异步资源。""" + return await self.async_create_batch_chat(**kwargs) + + async def async_create_batch_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Embedding", + self._ARK_BATCH_EMBEDDING_KEYS, + ("input",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.embeddings.create(**params) + ) + + async def async_create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Multimodal Embedding", + self._ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS, + ("input",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.multimodal_embeddings.create(**params) + ) + + async def async_create_context(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context", + self._ARK_CONTEXT_CREATE_KEYS, + ("messages",), + ) + return await self._resolve_async_result(self._get_aclient().context.create(**params)) + + async def async_context_complete(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context Completion", + self._ARK_CONTEXT_COMPLETION_KEYS, + ("context_id", "messages"), + ) + params["context_id"] = self._require_non_empty_string( + params["context_id"], + "Ark Context Completion 的 context_id", + ) + return await self._resolve_async_result( + self._get_aclient().context.completions.create(**params) + ) + + async def async_create_content_generation_task(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Content Generation", + self._ARK_CONTENT_GENERATION_CREATE_KEYS, + ("content",), + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.create(**params) + ) + + async def async_get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 获取", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.get(task_id=task_id, **kwargs) + ) + + async def async_list_content_generation_tasks(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Content Generation 列表", + kwargs, + self._ARK_CONTENT_GENERATION_LIST_KEYS, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.list(**kwargs) + ) + + async def async_delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 删除", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.delete(task_id=task_id, **kwargs) + ) + + async def async_count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + self._validate_resource_kwargs("Tokenization", kwargs, {"user", "extra_headers", "extra_query", "extra_body", "timeout"}) + response = await self._resolve_async_result(self._get_aclient().tokenization.create( + text=text, model=self._model_name, **kwargs + )) + values = field(response, "data", []) or [] + if not values: + raise RuntimeError("Ark tokenization 响应缺少 data。") + return sum(int(field(item, "total_tokens", 0)) for item in values) + + async def async_multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Multimodal Embedding", + kwargs, + {"encoding_format", "dimensions", "instructions", "sparse_embedding", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result(self._get_aclient().multimodal_embeddings.create( + input=inputs, model=self._model_name, **kwargs + )) + + async def async_generate_image(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Images", kwargs, self._ARK_IMAGE_GENERATE_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().images.generate(**kwargs)) + + async def async_beta_chat_parse(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Beta Chat Parse", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().beta.chat.completions.parse(**kwargs)) + + def async_beta_chat_stream(self, **kwargs: Any) -> Any: + """返回 Ark 异步 Beta Chat 的原生流式上下文管理器。 + + Ark SDK 的 ``AsyncCompletions.stream`` 是同步方法,但返回的对象实现 + ``__aenter__``/``__aexit__``。这里不能再包一层协程,否则调用方必须 + 先无意义地 await,无法直接按 SDK 约定使用 ``async with``。 + """ + self._validate_resource_kwargs("Beta Chat Stream", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_aclient().beta.chat.completions.stream(**kwargs) + + async def async_bot_chat(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Bot Chat", kwargs, self._ARK_BOT_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().bot_chat.completions.create(**kwargs)) + + async def _get_async_classification_resource(self) -> Any: + """返回 Ark Async Classification 资源,保持与同步入口相同的显式边界。""" + client = self._get_aclient() + resource = getattr(client, "classification", None) + if resource is not None: + return resource + resource = getattr(self, "_async_classification_resource", None) + if resource is not None: + return resource try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"火山引擎嵌入 ({self._model_name}) 异步完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"火山引擎嵌入 ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + from volcenginesdkarkruntime.resources.classification import ( # type: ignore[import-untyped] + AsyncClassification, + ) + except (ImportError, AttributeError) as exc: + raise NotImplementedError( + "当前 volcengine-python-sdk 未提供 Ark Async Classification 资源。" + ) from exc + resource = AsyncClassification(client) + self._async_classification_resource = resource + return resource + + async def async_classify( + self, + query: str, + labels: list[str], + model: str | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 Ark Classification 接口。""" + query = self._require_non_empty_string(query, "Ark Classification 的 query") + if not isinstance(labels, list) or not labels or any( + not isinstance(label, str) or not label.strip() for label in labels + ): + raise ValueError("Ark Classification 的 labels 必须是非空字符串列表。") + labels = [label.strip() for label in labels] + effective_model = ( + self._model_name + if model is None + else self._require_non_empty_string(model, "Ark Classification 的 model") + ) + self._validate_resource_kwargs( + "Classification", + kwargs, + {"user", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + resource = await self._get_async_classification_resource() + return await self._resolve_async_result(resource.create( + query=query, + model=effective_model, + labels=labels, + **kwargs, + )) + + def close(self) -> None: + """释放同步和异步 Ark SDK 客户端及分类资源。""" + client = self._client + async_client = self._aclient + self._client = None + self._classification_resource = None + self._async_classification_resource = None + first_error: Exception | None = None + closed_ids: set[int] = set() + if client is not None: + try: + client.close() + closed_ids.add(id(client)) + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + if id(async_client) in closed_ids: + self._aclient = None + else: + try: + close_resource_sync(async_client, "Volcengine 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放同步和异步 Ark SDK 客户端。""" + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + self._classification_resource = None + self._async_classification_resource = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error From 982cfdc099e8fe09daab5946aa053e8e01bd1e08 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:20 +0800 Subject: [PATCH 06/35] refactor: layer runtime services and entrypoint Move chat orchestration, retrieval and knowledge-build flows behind runtime service objects and slim main.py down to wiring. Also harden the embedding path while touching it: validate the row count returned by embed_documents against the input, route query embeddings through the provider-specific task_type, and close the embedding model on shutdown. Co-Authored-By: Claude Opus 5 (1M context) --- logo_ASCII.py | 11 +- main.py | 31 +-- src/chat/core.py | 320 ++++++++++++++++++------ src/runtime/__init__.py | 1 - src/runtime/contracts.py | 25 +- src/services/__init__.py | 1 - src/services/chat_service.py | 41 +-- src/services/embedding_service.py | 61 ++++- src/services/knowledge_build_service.py | 116 +++++---- src/services/retrieval_service.py | 263 +++++++++++++++---- src/ui/config_menu.py | 15 +- src/ui/display_utils.py | 16 +- 12 files changed, 654 insertions(+), 247 deletions(-) diff --git a/logo_ASCII.py b/logo_ASCII.py index 688dfe6..fe82e7b 100644 --- a/logo_ASCII.py +++ b/logo_ASCII.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- +import pyfiglet from rich.console import Console from rich.text import Text -import pyfiglet -from typing import Tuple console = Console() -def create_gradient(text: str, start_color: Tuple[int, int, int], end_color: Tuple[int, int, int]) -> Text: + +def create_gradient( + text: str, start_color: tuple[int, int, int], end_color: tuple[int, int, int] +) -> Text: """为文本创建从左到右的水平颜色渐变效果。""" text_obj = Text() total_length = len(text) @@ -31,4 +32,4 @@ def display_banner(): console.print(gradient_banner) if __name__ == "__main__": - display_banner() \ No newline at end of file + display_banner() diff --git a/main.py b/main.py index 53434ea..f271f04 100644 --- a/main.py +++ b/main.py @@ -1,35 +1,37 @@ -# -*- coding: utf-8 -*- -import sys import os -from io import StringIO +import sys from contextlib import redirect_stderr, redirect_stdout -from rich.console import Console -from rich.panel import Panel -from rich.text import Text +from io import StringIO + import pyfiglet -from typing import Tuple from prompt_toolkit import prompt from prompt_toolkit.formatted_text import HTML +from rich.console import Console +from rich.panel import Panel +from rich.text import Text os.environ.setdefault("PROMPT_TOOLKIT_NO_CPR", "1") # --- 绝对导入 --- # 导入 cleanup 模块以注册 atexit 钩子 import src.utils.cleanup -# 从新的配置模块导入 settings 实例 -from src.utils.config import get_settings, resolve_app_root + # 导入日志管理器,以便调用其清理函数 import src.utils.log_manager + # 导入UI工具 from src.ui.display_utils import CONSOLE_WIDTH +# 从新的配置模块导入 settings 实例 +from src.utils.config import get_settings, resolve_app_root + console = Console() -VERSION = "1.3.0" # 程序版本 +VERSION = "1.4.0" # 程序版本 # ================================================================= # 应用程序界面 (APP UI) # ================================================================= -def create_gradient(text: str, start_color: Tuple[int, int, int], end_color: Tuple[int, int, int]) -> Text: +def create_gradient(text: str, start_color: tuple[int, int, int], end_color: tuple[int, int, int]) -> Text: """为文本创建从左到右的水平颜色渐变效果。""" text_obj = Text() total_length = len(text) @@ -69,8 +71,7 @@ def display_banner(): # 计算填充,确保署名在右下角对齐 padding_size = banner_width - len(line_content) - len(attribution_text) - if padding_size < 1: - padding_size = 1 + padding_size = max(padding_size, 1) padding = Text(" " * padding_size) @@ -132,7 +133,7 @@ def initialize_dependencies(): import jieba.posseg as pseg jieba.setLogLevel(jieba.logging.ERROR) - setattr(jieba.dt, 'tmp_dir', str(cache_dir)) + jieba.dt.tmp_dir = str(cache_dir) list(pseg.cut("")) console.print("[dim]依赖项初始化完成。[/dim]") @@ -190,7 +191,7 @@ def main(): except (KeyboardInterrupt, EOFError): console.print("\n[bold yellow]检测到中断信号,正在退出程序... 再见![/bold yellow]") sys.exit(0) - except Exception as e: + except Exception as e: # noqa: BLE001 - top-level CLI boundary reports failures console.print(f"\n[bold red]程序运行期间发生错误:[/bold red] {e}") console.print("[bold red]请检查错误信息并重试。[/bold red]") diff --git a/src/chat/core.py b/src/chat/core.py index 215a80a..001de2d 100644 --- a/src/chat/core.py +++ b/src/chat/core.py @@ -1,7 +1,9 @@ -# -*- coding: utf-8 -*- import asyncio +import inspect import os -from typing import Any, AsyncGenerator, Dict, List, Optional +from collections.abc import AsyncGenerator +from copy import deepcopy +from typing import Any from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -13,10 +15,16 @@ os.environ.setdefault("PROMPT_TOOLKIT_NO_CPR", "1") +from ..providers.__base__.model_provider import LargeLanguageModel, close_resource_sync from ..providers.factory import ModelProviderFactory from ..retrieval.vdb.base import VectorStoreBase from ..retrieval.vdb.factory import VectorStoreFactory -from ..runtime.contracts import RunConfig, SessionConfig, build_run_config, build_session_config +from ..runtime.contracts import ( + RunConfig, + SessionConfig, + build_run_config, + build_session_config, +) from ..services.chat_service import ChatService from ..services.embedding_service import EmbeddingService from ..services.retrieval_service import RetrievalService @@ -24,6 +32,16 @@ from ..ui.display_utils import display_chat_config, get_relative_path from ..utils.config import get_settings from ..utils.log_manager import get_chat_logger +from ..utils.security import redact_sensitive_text + + +def _safe_exception_text(exc: BaseException) -> str: + """终端显示错误类型和上下文,同时遮蔽常见凭证格式。""" + message = str(exc).strip() + if not message: + return type(exc).__name__ + message = redact_sensitive_text(message) + return f"{type(exc).__name__}: {message[:240]}" class Chatbot: @@ -31,10 +49,10 @@ def __init__(self, console: Console): self.console = console self.run_config: RunConfig = build_run_config(get_settings()) self.session_config: SessionConfig = build_session_config(get_settings()) - self.vector_store: Optional[VectorStoreBase] = None - self.retrieval_service: Optional[RetrievalService] = None - self.llm_model = None - self.chat_service: Optional[ChatService] = None + self.vector_store: VectorStoreBase | None = None + self.retrieval_service: RetrievalService | None = None + self.llm_model: LargeLanguageModel | None = None + self.chat_service: ChatService | None = None self.logger = get_chat_logger() self._initialize_vector_store() @@ -45,7 +63,7 @@ def chat_config(self) -> SessionConfig: return self.session_config @chat_config.setter - def chat_config(self, value: SessionConfig | Dict[str, Any]): + def chat_config(self, value: SessionConfig | dict[str, Any]): if isinstance(value, SessionConfig): self.session_config = value return @@ -63,49 +81,177 @@ def _initialize_vector_store(self): self.console.print("[green]知识快照加载成功。[/green]") def reload_llm(self) -> bool: + previous_model = self.llm_model + new_model: LargeLanguageModel | None = None try: llm_key = self.session_config.active_llm_configuration - llm_model = ModelProviderFactory.get_llm_provider(llm_key) + llm_configurations = getattr(self.session_config, "llm_configurations", None) + if llm_configurations is None: + new_model = ModelProviderFactory.get_llm_provider(llm_key) + else: + new_model = ModelProviderFactory.get_llm_provider(llm_key, llm_configurations) if self.retrieval_service is None: raise RuntimeError("检索服务尚未初始化。") - chat_service = ChatService(llm_model, self.retrieval_service) - self.llm_model = llm_model + chat_service = ChatService(new_model, self.retrieval_service) + self.llm_model = new_model self.chat_service = chat_service + if previous_model is not None and previous_model is not new_model: + self._dispose_model(previous_model) return True - except Exception as exc: - self.console.print(f"[bold red]重载 LLM 出错: {exc}[/bold red]") + except Exception as exc: # noqa: BLE001 - reload boundary reports provider failures + if new_model is not None and new_model is not self.llm_model: + self._dispose_model(new_model) + self.console.print(f"[bold red]重载 LLM 出错: {_safe_exception_text(exc)}[/bold red]") return False - def apply_config_update(self, updated_config: SessionConfig | Dict[str, Any], llm_needs_reload: bool) -> None: - previous_llm = self.session_config.active_llm_configuration + def _dispose_model(self, model: LargeLanguageModel) -> None: + """关闭已替换或初始化失败的 Provider,避免连接池泄漏。""" + try: + close_resource_sync(model, "LLM Provider") + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭 LLM Provider 失败: %s", + redact_sensitive_text(str(exc)), + ) + + async def _adispose_model(self, model: LargeLanguageModel) -> None: + """在异步会话中释放同步、异步 SDK 客户端。""" + close = getattr(model, "aclose", None) + if callable(close): + try: + result = close() + if inspect.isawaitable(result): + await result + return + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "异步关闭 LLM Provider 失败: %s", + redact_sensitive_text(str(exc)), + ) + return + await asyncio.to_thread(self._dispose_model, model) + + def close(self) -> None: + model = self.llm_model + self.llm_model = None + self.chat_service = None + if model is not None: + self._dispose_model(model) + retrieval_service = self.retrieval_service + self.retrieval_service = None + close_retrieval = getattr(retrieval_service, "close", None) + if callable(close_retrieval): + try: + close_retrieval() + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭检索服务失败: %s", + redact_sensitive_text(str(exc)), + ) + + async def aclose(self) -> None: + model = self.llm_model + self.llm_model = None + self.chat_service = None + if model is not None: + await self._adispose_model(model) + retrieval_service = self.retrieval_service + self.retrieval_service = None + close_retrieval = getattr(retrieval_service, "aclose", None) + if callable(close_retrieval): + try: + result = close_retrieval() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "异步关闭检索服务失败: %s", + redact_sensitive_text(str(exc)), + ) + else: + close_retrieval = getattr(retrieval_service, "close", None) + if callable(close_retrieval): + try: + result = close_retrieval() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭检索服务失败: %s", + redact_sensitive_text(str(exc)), + ) + + def apply_config_update(self, updated_config: SessionConfig | dict[str, Any], llm_needs_reload: bool) -> None: + previous_config = deepcopy(self.session_config) if llm_needs_reload else None self.chat_config = updated_config if not llm_needs_reload: return if self.reload_llm(): return - self.session_config.active_llm_configuration = previous_llm + if previous_config is not None: + self.session_config = previous_config self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]") + async def apply_config_update_async( + self, + updated_config: SessionConfig | dict[str, Any], + llm_needs_reload: bool, + ) -> None: + """异步会话中的配置更新,避免同步 close 遗漏异步连接池。""" + previous_config = deepcopy(self.session_config) if llm_needs_reload else None + self.chat_config = updated_config + if not llm_needs_reload: + return + + previous_model = self.llm_model + new_model: LargeLanguageModel | None = None + try: + llm_key = self.session_config.active_llm_configuration + configurations = getattr(self.session_config, "llm_configurations", None) + new_model = ModelProviderFactory.get_llm_provider( + llm_key, configurations + ) if configurations is not None else ModelProviderFactory.get_llm_provider(llm_key) + if self.retrieval_service is None: + raise RuntimeError("检索服务尚未初始化。") + chat_service = ChatService(new_model, self.retrieval_service) + self.llm_model = new_model + self.chat_service = chat_service + if previous_model is not None and previous_model is not new_model: + await self._adispose_model(previous_model) + return + except Exception as exc: # noqa: BLE001 - reload boundary reports provider failures + if new_model is not None and new_model is not self.llm_model: + await self._adispose_model(new_model) + if previous_config is not None: + self.session_config = previous_config + self.console.print(f"[bold red]重载 LLM 出错: {_safe_exception_text(exc)}[/bold red]") + self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]") + async def _identify_intent_async(self, user_query: str) -> str: - if getattr(self, "chat_service", None) is None: + chat_service = getattr(self, "chat_service", None) + if chat_service is None: return user_query - return await self.chat_service.identify_intent(user_query) + return await chat_service.identify_intent(user_query) - async def _retrieve_knowledge_async(self, retrieval_query: str) -> List[Dict[str, Any]]: - if getattr(self, "retrieval_service", None) is None: + async def _retrieve_knowledge_async(self, retrieval_query: str) -> list[dict[str, Any]]: + retrieval_service = getattr(self, "retrieval_service", None) + if retrieval_service is None: return [] - return await self.retrieval_service.retrieve(retrieval_query, self.session_config, console=self.console) + return await retrieval_service.retrieve(retrieval_query, self.session_config, console=self.console) async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: - intent = await self._identify_intent_async(user_input) - retrieved_docs = await self._retrieve_knowledge_async(intent) - self.console.print(f" [bold]意图:[/bold] [yellow]{intent}[/yellow]") - self._display_retrieved_docs(retrieved_docs) - + intent = user_input + retrieved_docs: list[dict[str, Any]] = [] full_response = "" try: - if getattr(self, "chat_service", None) is not None: - async for chunk in self.chat_service.generate_reply( + intent = await self._identify_intent_async(user_input) + retrieved_docs = await self._retrieve_knowledge_async(intent) + self.console.print(f" [bold]意图:[/bold] [yellow]{intent}[/yellow]") + self._display_retrieved_docs(retrieved_docs) + + chat_service = getattr(self, "chat_service", None) + if chat_service is not None: + async for chunk in chat_service.generate_reply( user_input=user_input, intent=intent, documents=retrieved_docs, @@ -113,19 +259,21 @@ async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: ): full_response += chunk yield chunk - else: + elif (llm_model := getattr(self, "llm_model", None)) is not None: prompt = ( f"你是一个智能客服。用户问题: {user_input}\n" f"意图: {intent}\n" f"知识: {retrieved_docs}\n回答要简洁。" ) - async for chunk in self.llm_model.ainvoke( - prompt=prompt, - stream=True, - temperature=self.session_config["chat_temperature"], - ): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": True} + supports_sampling = getattr(llm_model, "supports_sampling_option", None) + if callable(supports_sampling) and supports_sampling("temperature"): + invoke_kwargs["temperature"] = self.session_config["chat_temperature"] + async for chunk in llm_model.ainvoke(**invoke_kwargs): full_response += chunk yield chunk + else: + raise RuntimeError("聊天服务尚未初始化。") self.logger.info( "Query: %s | Intent: %s | Docs: %s\nResponse: %s", user_input, @@ -134,10 +282,18 @@ async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: full_response, ) except Exception as exc: - self.console.print(f"[red]LLM 异步生成出错: {exc}[/red]") - yield "抱歉,生成回复时遇到错误。" + error_text = redact_sensitive_text(str(exc)) + self.logger.exception( + "聊天请求处理失败: %s", + error_text, + ) + self.console.print(f"[red]LLM 异步生成出错: {_safe_exception_text(exc)}[/red]") + # 已经输出部分内容时不要追加道歉文本,避免用户收到混合响应; + # 具体异常已通过日志和控制台显式记录。 + if not full_response: + yield "抱歉,处理请求时遇到错误。" - def _display_retrieved_docs(self, docs: List[Dict[str, Any]]): + def _display_retrieved_docs(self, docs: list[dict[str, Any]]): if not docs: self.console.print("[yellow]无相关文档。[/yellow]") return @@ -156,46 +312,62 @@ def _display_retrieved_docs(self, docs: List[Dict[str, Any]]): async def start_chat_session_async(): console = Console() bot = Chatbot(console) - session = PromptSession() + try: + session = PromptSession() - if bot.llm_model: - display_chat_config(console, bot.chat_config) - console.print(f"客服已就绪 ([bold green]{bot.chat_config['active_llm_configuration']}[/bold green])") + if bot.llm_model: + display_chat_config(console, bot.chat_config) + console.print(f"客服已就绪 ([bold green]{bot.chat_config['active_llm_configuration']}[/bold green])") - while True: - try: - user_query = await session.prompt_async(HTML('你: ')) - if not user_query.strip(): - continue - if user_query.lower() == "/quit": - break + while True: + try: + user_query = await session.prompt_async(HTML('你: ')) + if not user_query.strip(): + continue + if user_query.lower() == "/quit": + break - if user_query.lower() == "/config": - editable_config = ( - bot.chat_config.to_dict() - if isinstance(bot.chat_config, SessionConfig) - else dict(bot.chat_config) - ) - llm_needs_reload, updated_config = await asyncio.to_thread( - launch_config_editor, - editable_config, - ) - bot.apply_config_update(updated_config, llm_needs_reload) - display_chat_config(console, bot.chat_config) - continue - - response_panel = Panel("", title="客服", border_style="green") - full_response = "" - with Live(response_panel, console=console, refresh_per_second=10) as live: - async for chunk in bot.chat_async(user_query): - full_response += chunk - live.update(Panel(Text(full_response), title="客服", border_style="green")) - - except (KeyboardInterrupt, EOFError): - break - console.print("[yellow]感谢使用,再见![/yellow]") - else: - console.print("[red]模型初始化失败。[/red]") + if user_query.lower() == "/config": + editable_config = ( + bot.chat_config.to_dict() + if isinstance(bot.chat_config, SessionConfig) + else dict(bot.chat_config) + ) + llm_needs_reload, updated_config = await asyncio.to_thread( + launch_config_editor, + editable_config, + ) + # 真实 Chatbot 使用异步重载以正确释放 SDK 连接池;保留 + # 同步兼容入口,便于嵌入方或旧版替身实现配置编辑。 + apply_async = getattr(bot, "apply_config_update_async", None) + if callable(apply_async): + result = apply_async(updated_config, llm_needs_reload) + if inspect.isawaitable(result): + await result + else: + apply_sync = getattr(bot, "apply_config_update", None) + if not callable(apply_sync): + raise AttributeError("Chatbot 缺少配置更新方法。") + apply_sync(updated_config, llm_needs_reload) + display_chat_config(console, bot.chat_config) + continue + + response_panel = Panel("", title="客服", border_style="green") + full_response = "" + with Live(response_panel, console=console, refresh_per_second=10) as live: + async for chunk in bot.chat_async(user_query): + full_response += chunk + live.update(Panel(Text(full_response), title="客服", border_style="green")) + + except (KeyboardInterrupt, EOFError): + break + console.print("[yellow]感谢使用,再见![/yellow]") + else: + console.print("[red]模型初始化失败。[/red]") + finally: + close = getattr(bot, "aclose", None) + if close is not None: + await close() def start_chat_session(): diff --git a/src/runtime/__init__.py b/src/runtime/__init__.py index 633f866..8b13789 100644 --- a/src/runtime/__init__.py +++ b/src/runtime/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- diff --git a/src/runtime/contracts.py b/src/runtime/contracts.py index ae703c3..808cbfc 100644 --- a/src/runtime/contracts.py +++ b/src/runtime/contracts.py @@ -1,15 +1,14 @@ -# -*- coding: utf-8 -*- from __future__ import annotations +from collections.abc import Iterator, MutableMapping from copy import deepcopy from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path -from typing import Any, Dict, Iterator, MutableMapping +from typing import Any from src.utils.config import ModelDetail, RetrievalMethod, Settings - SCHEMA_VERSION = "2" @@ -24,9 +23,9 @@ class RunConfig: default_llm_provider: str default_embedding_provider: str default_rerank_provider: str - llm_configurations: Dict[str, ModelDetail] - embedding_configurations: Dict[str, ModelDetail] - rerank_configurations: Dict[str, ModelDetail] + llm_configurations: dict[str, ModelDetail] + embedding_configurations: dict[str, ModelDetail] + rerank_configurations: dict[str, ModelDetail] chat_temperature: float kb_embedding_batch_size: int kb_chunk_size: int @@ -52,8 +51,8 @@ def __init__( score_threshold: float, active_llm_configuration: str, active_rerank_configuration: str, - llm_configurations: Dict[str, ModelDetail], - rerank_configurations: Dict[str, ModelDetail], + llm_configurations: dict[str, ModelDetail], + rerank_configurations: dict[str, ModelDetail], chat_temperature: float, ): self.retrieval_method = retrieval_method @@ -85,7 +84,7 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self.to_dict()) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: return { "retrieval_method": self.retrieval_method, "vector_weight": self.vector_weight, @@ -127,11 +126,11 @@ def create( source_digest: str, document_count: int, chunk_count: int, - ) -> "KnowledgeSnapshotManifest": + ) -> KnowledgeSnapshotManifest: return cls( schema_version=SCHEMA_VERSION, snapshot_id=snapshot_id, - created_at=datetime.now(timezone.utc).isoformat(), + created_at=datetime.now(UTC).isoformat(), store_type=store_type, embedding_provider=embedding_provider, embedding_model=embedding_model, @@ -159,7 +158,7 @@ def to_toml(self) -> str: ) @classmethod - def from_mapping(cls, data: Dict[str, Any]) -> "KnowledgeSnapshotManifest": + def from_mapping(cls, data: dict[str, Any]) -> KnowledgeSnapshotManifest: return cls( schema_version=str(data["schema_version"]), snapshot_id=str(data["snapshot_id"]), diff --git a/src/services/__init__.py b/src/services/__init__.py index 633f866..8b13789 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- diff --git a/src/services/chat_service.py b/src/services/chat_service.py index 279b5c4..4cc7aa6 100644 --- a/src/services/chat_service.py +++ b/src/services/chat_service.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- from __future__ import annotations -from typing import Any, AsyncGenerator, Dict, List +from collections.abc import AsyncGenerator +from typing import Any from src.providers.__base__.model_provider import LargeLanguageModel from src.runtime.contracts import SessionConfig from src.services.retrieval_service import RetrievalService from src.utils.log_manager import get_chat_logger +from src.utils.security import redact_sensitive_text class ChatService: @@ -15,18 +16,29 @@ def __init__(self, llm_model: LargeLanguageModel, retrieval_service: RetrievalSe self.retrieval_service = retrieval_service self.logger = get_chat_logger() + def _temperature_kwargs(self, temperature: float) -> dict[str, Any]: + supports_sampling = getattr(self.llm_model, "supports_sampling_option", None) + if callable(supports_sampling) and supports_sampling("temperature"): + return {"temperature": temperature} + return {} + async def identify_intent(self, user_query: str) -> str: prompt = f"你是一个意图识别助手。判断用户意图。\n用户问题: {user_query}\n直接输出意图简述。" response = "" try: - async for chunk in self.llm_model.ainvoke(prompt=prompt, stream=False, temperature=0.1): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": False} + invoke_kwargs.update(self._temperature_kwargs(0.1)) + async for chunk in self.llm_model.ainvoke(**invoke_kwargs): response += chunk except Exception as exc: - self.logger.error("异步意图识别失败: %s", exc) - return user_query - return response.strip() or user_query + self.logger.error("异步意图识别失败: %s", redact_sensitive_text(str(exc))) + raise RuntimeError("意图识别失败,无法继续检索。") from exc + intent = response.strip() + if not intent: + raise RuntimeError("意图识别返回空结果,无法继续检索。") + return intent - async def retrieve(self, user_input: str, session_config: SessionConfig, console: Any) -> tuple[str, List[Dict[str, Any]]]: + async def retrieve(self, user_input: str, session_config: SessionConfig, console: Any) -> tuple[str, list[dict[str, Any]]]: intent = await self.identify_intent(user_input) documents = await self.retrieval_service.retrieve(intent, session_config, console=console) return intent, documents @@ -35,21 +47,20 @@ async def generate_reply( self, user_input: str, intent: str, - documents: List[Dict[str, Any]], + documents: list[dict[str, Any]], session_config: SessionConfig, ) -> AsyncGenerator[str, None]: prompt = self._build_prompt(user_input, intent, documents) - async for chunk in self.llm_model.ainvoke( - prompt=prompt, - stream=True, - temperature=session_config.chat_temperature, - ): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": True} + invoke_kwargs.update(self._temperature_kwargs(session_config.chat_temperature)) + async for chunk in self.llm_model.ainvoke(**invoke_kwargs): yield chunk @staticmethod - def _build_prompt(user_input: str, intent: str, documents: List[Dict[str, Any]]) -> str: + def _build_prompt(user_input: str, intent: str, documents: list[dict[str, Any]]) -> str: context = "\n".join( - f"来源: {doc['metadata']['source']}\n内容: {doc.get('page_content', '')}" + f"来源: {(doc.get('metadata') or {}).get('source') or '未知来源'}\n" + f"内容: {doc.get('page_content', '')}" for doc in documents ) if context: diff --git a/src/services/embedding_service.py b/src/services/embedding_service.py index bd8fc03..d876e7b 100644 --- a/src/services/embedding_service.py +++ b/src/services/embedding_service.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- from __future__ import annotations -import asyncio -from typing import Any, List +import inspect +from typing import Any import numpy as np +from src.providers.__base__.model_provider import close_resource_sync from src.providers.factory import ModelProviderFactory from src.runtime.contracts import RunConfig @@ -25,21 +25,29 @@ def embedding_model_detail(self): def _get_model(self): if self._embedding_model is None: - self._embedding_model = ModelProviderFactory.get_embedding_provider(self.embedding_provider_key) + self._embedding_model = ModelProviderFactory.get_embedding_provider( + self.embedding_provider_key, + self.run_config.embedding_configurations, + ) return self._embedding_model - async def embed_texts(self, texts: List[str]) -> np.ndarray: + async def embed_texts(self, texts: list[str]) -> np.ndarray: model = self._get_model() embeddings = await model.aembed_documents(texts) - return self._as_float32_matrix(embeddings) + return self._as_float32_matrix(embeddings, expected_rows=len(texts)) async def embed_query(self, query: str) -> np.ndarray: - embeddings = await self.embed_texts([query]) - return embeddings[0] + model = self._get_model() + options: dict[str, Any] = {} + if self.embedding_model_detail.provider == "google": + options.setdefault("task_type", "RETRIEVAL_QUERY") + vector = await model.aembed_query(query, **options) + matrix = self._as_float32_matrix([vector], expected_rows=1) + return matrix[0] - async def embed_in_batches(self, texts: List[str]) -> np.ndarray: + async def embed_in_batches(self, texts: list[str]) -> np.ndarray: batch_size = max(1, self.run_config.kb_embedding_batch_size) - batches: List[np.ndarray] = [] + batches: list[np.ndarray] = [] for start in range(0, len(texts), batch_size): batch = texts[start : start + batch_size] batches.append(await self.embed_texts(batch)) @@ -47,11 +55,42 @@ async def embed_in_batches(self, texts: List[str]) -> np.ndarray: return np.empty((0, 0), dtype=np.float32) return np.vstack(batches) + def close(self) -> None: + """释放按需创建的 Embedding Provider。""" + model = self._embedding_model + self._embedding_model = None + if model is None: + return + close_resource_sync(model, "Embedding Provider") + + async def aclose(self) -> None: + """异步释放按需创建的 Embedding Provider。""" + model = self._embedding_model + self._embedding_model = None + if model is None: + return + close = getattr(model, "aclose", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + return + close = getattr(model, "close", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + @staticmethod - def _as_float32_matrix(embeddings: Any) -> np.ndarray: + def _as_float32_matrix(embeddings: Any, expected_rows: int | None = None) -> np.ndarray: matrix = np.array(embeddings, dtype=np.float32) if matrix.ndim != 2: raise ValueError("EmbeddingService 期望二维向量矩阵。") if matrix.shape[0] == 0 or matrix.shape[1] == 0: raise ValueError("EmbeddingService 收到空向量矩阵。") + if expected_rows is not None and matrix.shape[0] != expected_rows: + raise ValueError( + "EmbeddingService 收到的向量数量与输入文本数量不一致。" + f" 输入 {expected_rows} 条,返回 {matrix.shape[0]} 条。" + ) return matrix diff --git a/src/services/knowledge_build_service.py b/src/services/knowledge_build_service.py index 3c84b0e..231da63 100644 --- a/src/services/knowledge_build_service.py +++ b/src/services/knowledge_build_service.py @@ -1,9 +1,11 @@ -# -*- coding: utf-8 -*- from __future__ import annotations import hashlib +from collections.abc import Iterable from pathlib import Path -from typing import Dict, Iterable, List +from typing import Any + +import numpy as np from src.etl.pipeline import Pipeline from src.models.document import Document @@ -29,65 +31,87 @@ def __init__( self.embedding_service = embedding_service self.snapshot_repository = snapshot_repository - async def build(self, splitter_structure_mode: str) -> Dict[str, object]: + async def build(self, splitter_structure_mode: str) -> dict[str, object]: markdown_files = sorted(self.run_config.knowledge_base_path.glob("*.md")) if not markdown_files: raise FileNotFoundError(f"知识库目录 '{self.run_config.knowledge_base_path}' 中未找到 Markdown 文件。") snapshot_id = self.snapshot_repository.generate_snapshot_id() temp_dir = self.snapshot_repository.create_temp_snapshot_dir(snapshot_id) - pipeline = Pipeline.from_file_path(markdown_files[0], splitter_structure_mode=splitter_structure_mode) + finalized = False + try: + pipeline = Pipeline.from_file_path(markdown_files[0], splitter_structure_mode=splitter_structure_mode) - chunk_count = 0 - for file_path in markdown_files: - chunks = self._process_file(pipeline, file_path, splitter_structure_mode) - await self._persist_chunks(chunks) - chunk_count += len(chunks) + chunk_count = 0 + pending_documents: list[dict[str, Any]] = [] + pending_embeddings: list[np.ndarray] = [] + pending_parent_documents: dict[str, dict[str, Any]] = {} + for file_path in markdown_files: + chunks = self._process_file(pipeline, file_path, splitter_structure_mode) + if splitter_structure_mode == "hierarchical" and hasattr(pipeline.splitter, "parent_documents"): + parent_documents = getattr(pipeline.splitter, "parent_documents", {}) + if isinstance(parent_documents, dict): + pending_parent_documents.update(parent_documents) + if chunks: + documents = [ + {"page_content": chunk.content, "metadata": chunk.metadata} + for chunk in chunks + ] + texts = [chunk.content for chunk in chunks] + pending_documents.extend(documents) + pending_embeddings.append(await self.embedding_service.embed_in_batches(texts)) + chunk_count += len(chunks) - manifest = KnowledgeSnapshotManifest.create( - snapshot_id=snapshot_id, - store_type=self.run_config.default_vector_store, - embedding_provider=self.embedding_service.embedding_provider_key, - embedding_model=self.embedding_service.embedding_model_detail.model_name, - chunk_mode=splitter_structure_mode, - source_digest=self._compute_source_digest(markdown_files), - document_count=len(markdown_files), - chunk_count=chunk_count, - ) - self.vector_store.save_snapshot(str(temp_dir)) - self.snapshot_repository.write_manifest(temp_dir, manifest) - self.snapshot_repository.finalize_snapshot(temp_dir, snapshot_id) - final_dir = self.snapshot_repository.get_active_snapshot_dir() - if final_dir is None: - raise RuntimeError("活动知识快照切换失败。") - self.snapshot_repository.validate_snapshot_dir(final_dir) - return { - "snapshot_id": snapshot_id, - "snapshot_dir": str(final_dir), - "document_count": len(markdown_files), - "chunk_count": chunk_count, - "embedding_provider": self.embedding_service.embedding_provider_key, - "embedding_model": self.embedding_service.embedding_model_detail.model_name, - "chunk_mode": splitter_structure_mode, - } + if pending_documents: + embeddings = np.vstack(pending_embeddings) + if len(pending_documents) != len(embeddings): + raise ValueError("知识快照中的文档数量与 embedding 数量不一致。") + self.vector_store.upsert_embeddings(pending_documents, embeddings) + if pending_parent_documents: + self.vector_store.register_parent_documents(pending_parent_documents) - def _process_file(self, pipeline: Pipeline, file_path: Path, splitter_structure_mode: str) -> List[Document]: + manifest = KnowledgeSnapshotManifest.create( + snapshot_id=snapshot_id, + store_type=self.run_config.default_vector_store, + embedding_provider=self.embedding_service.embedding_provider_key, + embedding_model=self.embedding_service.embedding_model_detail.model_name, + chunk_mode=splitter_structure_mode, + source_digest=self._compute_source_digest(markdown_files), + document_count=len(markdown_files), + chunk_count=chunk_count, + ) + self.vector_store.save_snapshot(str(temp_dir)) + self.snapshot_repository.write_manifest(temp_dir, manifest) + self.snapshot_repository.validate_snapshot_dir(temp_dir) + self.snapshot_repository.finalize_snapshot(temp_dir, snapshot_id) + final_dir = self.snapshot_repository.get_active_snapshot_dir() + if final_dir is None: + raise RuntimeError("活动知识快照切换失败。") + self.snapshot_repository.validate_snapshot_dir(final_dir) + finalized = True + return { + "snapshot_id": snapshot_id, + "snapshot_dir": str(final_dir), + "document_count": len(markdown_files), + "chunk_count": chunk_count, + "embedding_provider": self.embedding_service.embedding_provider_key, + "embedding_model": self.embedding_service.embedding_model_detail.model_name, + "chunk_mode": splitter_structure_mode, + } + finally: + if not finalized: + try: + self.snapshot_repository.cleanup_temp_snapshot_dir(snapshot_id) + except Exception: + logger.exception("清理临时知识快照失败: %s", temp_dir) + + def _process_file(self, pipeline: Pipeline, file_path: Path, splitter_structure_mode: str) -> list[Document]: logger.info("正在处理文件: %s", file_path) content = file_path.read_text(encoding="utf-8") document = Document(content=content, metadata={"source": str(file_path)}) chunks = pipeline.process(document) - if splitter_structure_mode == "hierarchical" and hasattr(pipeline.splitter, "parent_documents"): - self.vector_store.register_parent_documents(getattr(pipeline.splitter, "parent_documents", {})) return chunks - async def _persist_chunks(self, chunks: List[Document]) -> None: - if not chunks: - return - documents = [{"page_content": chunk.content, "metadata": chunk.metadata} for chunk in chunks] - texts = [doc["page_content"] for doc in documents] - embeddings = await self.embedding_service.embed_in_batches(texts) - self.vector_store.upsert_embeddings(documents, embeddings) - @staticmethod def _compute_source_digest(markdown_files: Iterable[Path]) -> str: digest = hashlib.sha256() diff --git a/src/services/retrieval_service.py b/src/services/retrieval_service.py index bc530d2..740365f 100644 --- a/src/services/retrieval_service.py +++ b/src/services/retrieval_service.py @@ -1,11 +1,16 @@ -# -*- coding: utf-8 -*- from __future__ import annotations +import asyncio import copy -from typing import Any, Callable, Dict, List, Optional +import inspect +import json +import math +from collections.abc import Callable, Mapping +from typing import Any from rich.console import Console +from src.providers.__base__.model_provider import RerankModel, close_resource_sync from src.providers.factory import ModelProviderFactory from src.retrieval.vdb.base import VectorStoreBase from src.runtime.contracts import SessionConfig @@ -27,7 +32,7 @@ def __init__(self, vector_weight: float, keyword_weight: float, fusion_strategy: raise ValueError(f"不支持的混合检索融合策略: {fusion_strategy}") @staticmethod - def _normalize_scores(scores: List[float]) -> List[float]: + def _normalize_scores(scores: list[float]) -> list[float]: if not scores: return [] max_score = max(scores) @@ -37,7 +42,7 @@ def _normalize_scores(scores: List[float]) -> List[float]: score_range = max_score - min_score return [max(0.0, min(1.0, (score - min_score) / score_range)) for score in scores] - def rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: if self.fusion_strategy == "weighted": return self._weighted_rerank(documents) return self._rrf_rerank(documents) @@ -48,7 +53,7 @@ def _weight_tuple(self) -> tuple[float, float]: return 0.0, 0.0 return self.vector_weight / total, self.keyword_weight / total - def _weighted_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _weighted_rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: vector_weight, keyword_weight = self._weight_tuple() scored_documents = [copy.deepcopy(doc) for doc in documents] keyword_scores = [float(doc.get("keyword_score", 0) or 0) for doc in scored_documents] @@ -59,7 +64,7 @@ def _weighted_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, An doc["score"] = vector_weight * normalized_semantic[index] + keyword_weight * normalized_keyword[index] return sorted(scored_documents, key=lambda item: item["score"], reverse=True) - def _rrf_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _rrf_rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: vector_weight, keyword_weight = self._weight_tuple() scored_documents = [copy.deepcopy(doc) for doc in documents] for doc in scored_documents: @@ -71,7 +76,7 @@ def _rrf_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return sorted(scored_documents, key=lambda item: item["score"], reverse=True) -def _document_key(document: Dict[str, Any]) -> str: +def _document_key(document: dict[str, Any]) -> str: metadata = document.get("metadata") or {} chunk_id = metadata.get("chunk_id") or metadata.get("doc_id") if chunk_id: @@ -83,8 +88,8 @@ def _document_key(document: Dict[str, Any]) -> str: return f"{source}:{document.get('page_content', '')[:64]}" -def _merge_hybrid_results(semantic_results: List[Dict[str, Any]], keyword_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - merged: Dict[str, Dict[str, Any]] = {} +def _merge_hybrid_results(semantic_results: list[dict[str, Any]], keyword_results: list[dict[str, Any]]) -> list[dict[str, Any]]: + merged: dict[str, dict[str, Any]] = {} for rank, document in enumerate(semantic_results, start=1): merged_doc = copy.deepcopy(document) merged_doc["semantic_score"] = float(merged_doc.get("score", 0) or 0) @@ -105,8 +110,8 @@ def _merge_hybrid_results(semantic_results: List[Dict[str, Any]], keyword_result return list(merged.values()) -def _promote_parent_context(documents: List[Dict[str, Any]], resolver: Optional[Callable[[str | None], Optional[str]]]) -> List[Dict[str, Any]]: - promoted_documents: List[Dict[str, Any]] = [] +def _promote_parent_context(documents: list[dict[str, Any]], resolver: Callable[[str | None], str | None] | None) -> list[dict[str, Any]]: + promoted_documents: list[dict[str, Any]] = [] for document in documents: promoted_doc = copy.deepcopy(document) metadata = promoted_doc.get("metadata") or {} @@ -119,8 +124,8 @@ def _promote_parent_context(documents: List[Dict[str, Any]], resolver: Optional[ return promoted_documents -def _deduplicate_parent_documents(documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - deduplicated: Dict[str, Dict[str, Any]] = {} +def _deduplicate_parent_documents(documents: list[dict[str, Any]]) -> list[dict[str, Any]]: + deduplicated: dict[str, dict[str, Any]] = {} for document in documents: metadata = document.get("metadata") or {} parent_key = metadata.get("parent_id") @@ -135,8 +140,9 @@ class RetrievalService: def __init__(self, vector_store: VectorStoreBase, embedding_service: EmbeddingService): self.vector_store = vector_store self.embedding_service = embedding_service + self._rerank_providers: dict[tuple[str, str, str, str], RerankModel] = {} - async def retrieve(self, query: str, session_config: SessionConfig, console: Optional[Console] = None) -> List[Dict[str, Any]]: + async def retrieve(self, query: str, session_config: SessionConfig, console: Console | None = None) -> list[dict[str, Any]]: retrieval_method = session_config.retrieval_method effective_top_k = max(1, session_config.top_k) * max(1, session_config.retrieval_candidate_multiplier) parent_resolver = getattr(self.vector_store, "resolve_parent_content", None) @@ -160,56 +166,213 @@ async def retrieve(self, query: str, session_config: SessionConfig, console: Opt ranked_results = _promote_parent_context(ranked_results, parent_resolver) ranked_results = _deduplicate_parent_documents(ranked_results) ranked_results = await self._rerank_if_needed(query, ranked_results, session_config) - return ranked_results[: session_config.top_k] + return ranked_results[: max(1, session_config.top_k)] - async def _gather_hybrid_results(self, query: str, top_k: int) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: - semantic_results = await self._semantic_retrieve(query, top_k) - keyword_results = await self._keyword_retrieve(query, top_k) + async def _gather_hybrid_results(self, query: str, top_k: int) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + semantic_results, keyword_results = await asyncio.gather( + self._semantic_retrieve(query, top_k), + self._keyword_retrieve(query, top_k), + ) return semantic_results, keyword_results - async def _semantic_retrieve(self, query: str, top_k: int) -> List[Dict[str, Any]]: - try: - query_embedding = await self.embedding_service.embed_query(query) - return await self._call_vector_store("semantic_search", query_embedding, top_k) - except (AttributeError, NotImplementedError): + async def _semantic_retrieve(self, query: str, top_k: int) -> list[dict[str, Any]]: + # Embedding failures must propagate. Only use the legacy path when the + # vector store truly does not expose the newer semantic_search method; + # an AttributeError/NotImplementedError raised inside a real provider is + # a defect or an upstream failure, not a compatibility signal. + if self._get_vector_store_method("semantic_search") is None: return await self._legacy_search(query, top_k, "semantic") + query_embedding = await self.embedding_service.embed_query(query) + return await self._call_vector_store("semantic_search", query_embedding, top_k) - async def _keyword_retrieve(self, query: str, top_k: int) -> List[Dict[str, Any]]: - try: - return await self._call_vector_store("keyword_search", query, top_k) - except (AttributeError, NotImplementedError): + async def _keyword_retrieve(self, query: str, top_k: int) -> list[dict[str, Any]]: + if self._get_vector_store_method("keyword_search") is None: return await self._legacy_search(query, top_k, "keyword") - - async def _call_vector_store(self, method_name: str, *args) -> List[Dict[str, Any]]: - import asyncio - - method = getattr(self.vector_store, method_name) + return await self._call_vector_store("keyword_search", query, top_k) + + def _get_vector_store_method(self, method_name: str) -> Callable[..., Any] | None: + """Return an implemented vector-store method, excluding base stubs.""" + method = getattr(self.vector_store, method_name, None) + if not callable(method): + return None + implementation = getattr(type(self.vector_store), method_name, None) + base_implementation = getattr(VectorStoreBase, method_name, None) + if implementation is base_implementation: + return None + return method + + async def _call_vector_store(self, method_name: str, *args) -> list[dict[str, Any]]: + method = self._get_vector_store_method(method_name) + if method is None: + raise NotImplementedError(f"向量存储未实现 {method_name} 接口。") return await asyncio.to_thread(method, *args) - async def _legacy_search(self, query: str, top_k: int, search_type: str) -> List[Dict[str, Any]]: - import asyncio - - if hasattr(self.vector_store, "asearch"): - return await self.vector_store.asearch(query, top_k, search_type=search_type) - return await asyncio.to_thread(self.vector_store.search, query, top_k, search_type) - - async def _rerank_if_needed(self, query: str, ranked_results: List[Dict[str, Any]], session_config: SessionConfig) -> List[Dict[str, Any]]: + async def _legacy_search(self, query: str, top_k: int, search_type: str) -> list[dict[str, Any]]: + async_search = getattr(self.vector_store, "asearch", None) + if callable(async_search): + return await async_search(query, top_k, search_type=search_type) + sync_search = getattr(self.vector_store, "search", None) + if not callable(sync_search): + raise NotImplementedError("向量存储未实现兼容 search 接口。") + return await asyncio.to_thread(sync_search, query, top_k, search_type) + + def _get_rerank_provider( + self, + provider_key: str, + configurations: Mapping[str, Any] | None = None, + ) -> RerankModel: + detail = configurations.get(provider_key) if configurations is not None else None + options = getattr(detail, "options", {}) if detail is not None else {} + try: + options_fingerprint = json.dumps( + options or {}, sort_keys=True, default=str, separators=(",", ":") + ) + except (TypeError, ValueError): + options_fingerprint = repr(options) + cache_key = ( + provider_key, + getattr(detail, "provider", ""), + getattr(detail, "model_name", ""), + options_fingerprint, + ) + provider = self._rerank_providers.get(cache_key) + if provider is None: + # A configuration reload can keep the same key while changing the + # endpoint, model or options. Drop the old instance before creating + # its replacement so stale transports and credentials are not kept + # alive indefinitely. + stale_keys = [ + key for key in self._rerank_providers + if key[0] == provider_key and key != cache_key + ] + for stale_key in stale_keys: + stale_provider = self._rerank_providers.pop(stale_key) + close_resource_sync(stale_provider, "Rerank Provider") + if configurations is None: + provider = ModelProviderFactory.get_rerank_provider(provider_key) + else: + provider = ModelProviderFactory.get_rerank_provider(provider_key, configurations) + self._rerank_providers[cache_key] = provider + return provider + + def clear_rerank_cache(self) -> None: + """关闭并清空已缓存的 Rerank Provider。""" + providers = list(self._rerank_providers.values()) + self._rerank_providers.clear() + closed: set[int] = set() + for provider in providers: + if id(provider) in closed: + continue + closed.add(id(provider)) + close_resource_sync(provider, "Rerank Provider") + + def close(self) -> None: + """释放重排缓存及 EmbeddingService 持有的 Provider。""" + self.clear_rerank_cache() + close_embedding = getattr(self.embedding_service, "close", None) + if callable(close_embedding): + close_embedding() + + async def aclose(self) -> None: + """异步释放重排缓存及 EmbeddingService 持有的 Provider。""" + providers = list(self._rerank_providers.values()) + self._rerank_providers.clear() + closed: set[int] = set() + for provider in providers: + if id(provider) in closed: + continue + closed.add(id(provider)) + close = getattr(provider, "aclose", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + continue + close = getattr(provider, "close", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + close_embedding = getattr(self.embedding_service, "aclose", None) + if not callable(close_embedding): + close_embedding = getattr(self.embedding_service, "close", None) + if callable(close_embedding): + result = close_embedding() + if inspect.isawaitable(result): + await result + + async def _rerank_if_needed(self, query: str, ranked_results: list[dict[str, Any]], session_config: SessionConfig) -> list[dict[str, Any]]: if not session_config.rerank_enabled or not ranked_results: return ranked_results - rerank_provider = ModelProviderFactory.get_rerank_provider(session_config.active_rerank_configuration) + rerank_provider = self._get_rerank_provider( + session_config.active_rerank_configuration, + session_config.rerank_configurations, + ) docs_to_rerank = [doc.get("page_content", "") for doc in ranked_results] - reranked_indices, reranked_scores = await rerank_provider.arerank(query, docs_to_rerank, top_n=session_config.top_k) - reranked_docs: List[Dict[str, Any]] = [] + # Keep the requested output bounded by the available candidates after + # thresholding and parent de-duplication. + rerank_top_n = max(1, min(session_config.top_k, len(docs_to_rerank))) + reranked_indices, reranked_scores = await rerank_provider.arerank( + query, docs_to_rerank, top_n=rerank_top_n + ) + reranked_indices, reranked_scores = self._validate_rerank_output( + reranked_indices, + reranked_scores, + document_count=len(ranked_results), + ) + reranked_docs: list[dict[str, Any]] = [] for index, score in zip(reranked_indices, reranked_scores): - if index < len(ranked_results): - document = copy.deepcopy(ranked_results[index]) - document["score"] = score - reranked_docs.append(document) + document = copy.deepcopy(ranked_results[index]) + document["score"] = score + reranked_docs.append(document) return sorted(reranked_docs, key=lambda item: item["score"], reverse=True) + @staticmethod + def _validate_rerank_output( + indices: Any, + scores: Any, + document_count: int, + ) -> tuple[list[int], list[float]]: + """Validate a provider result before it can alter retrieval ordering.""" + try: + normalized_indices = list(indices) + normalized_scores = list(scores) + except TypeError as exc: + raise RuntimeError("Rerank provider 返回值必须是可迭代的索引和分数。") from exc + + if len(normalized_indices) != len(normalized_scores): + raise RuntimeError("Rerank provider 返回的索引和分数数量不一致。") + if not normalized_indices: + raise RuntimeError("Rerank provider 未返回可用结果。") + + seen_indices: set[int] = set() + validated_scores: list[float] = [] + for position, (index, score) in enumerate(zip(normalized_indices, normalized_scores)): + if ( + isinstance(index, bool) + or not isinstance(index, int) + or not 0 <= index < document_count + ): + raise RuntimeError(f"Rerank provider 返回的 index 无效: position={position}。") + if index in seen_indices: + raise RuntimeError(f"Rerank provider 返回重复 index: {index}。") + seen_indices.add(index) + if isinstance(score, bool): + raise RuntimeError(f"Rerank provider 返回的 score 无效: index={index}。") + try: + normalized_score = float(score) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"Rerank provider 返回的 score 无效: index={index}。") from exc + if not math.isfinite(normalized_score): + raise RuntimeError(f"Rerank provider 返回的 score 必须是有限数值: index={index}。") + validated_scores.append(normalized_score) + + return normalized_indices, validated_scores + @staticmethod def _should_apply_score_threshold(session_config: SessionConfig) -> bool: - if session_config.retrieval_method == RetrievalMethod.HYBRID_SEARCH and session_config.hybrid_fusion_strategy == "rrf": - return False - return True + return not ( + session_config.retrieval_method == RetrievalMethod.HYBRID_SEARCH + and session_config.hybrid_fusion_strategy == "rrf" + ) diff --git a/src/ui/config_menu.py b/src/ui/config_menu.py index 6dfa5a5..a00d54b 100644 --- a/src/ui/config_menu.py +++ b/src/ui/config_menu.py @@ -1,17 +1,17 @@ -# -*- coding: utf-8 -*- +from typing import Any + import questionary from rich.console import Console from rich.panel import Panel -from typing import Dict, Any, Tuple -from ..utils.config import get_settings, RetrievalMethod # 导入 get_settings 函数 +from ..utils.config import RetrievalMethod +from ..utils.log_manager import get_module_logger # 导入日志管理器 from .display_utils import display_chat_config -from ..utils.log_manager import get_module_logger # 导入日志管理器 logger = get_module_logger(__name__) # 获取当前模块的日志器 console = Console() -def edit_retrieval_params(chat_config: Dict[str, Any]) -> None: +def edit_retrieval_params(chat_config: dict[str, Any]) -> None: """编辑检索相关参数。""" logger.info("进入检索参数编辑菜单。") while True: @@ -132,9 +132,8 @@ def is_float_between_0_and_1(text): console.print(f"[green]候选过量招募倍率已更新为: {new_multiplier}[/green]") logger.info(f"候选过量招募倍率已更新为: {new_multiplier}") # 检索参数的更改不需要重载任何模型 - return None -def edit_model_params(chat_config: Dict[str, Any]) -> bool: +def edit_model_params(chat_config: dict[str, Any]) -> bool: """编辑模型相关参数。""" logger.info("进入模型参数编辑菜单。") llm_changed = False @@ -185,7 +184,7 @@ def edit_model_params(chat_config: Dict[str, Any]) -> bool: return llm_changed -def launch_config_editor(chat_config: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]: +def launch_config_editor(chat_config: dict[str, Any]) -> tuple[bool, dict[str, Any]]: """ 启动交互式配置编辑器。 返回一个元组 (llm_needs_reload, updated_config) diff --git a/src/ui/display_utils.py b/src/ui/display_utils.py index 24edb77..21b5117 100644 --- a/src/ui/display_utils.py +++ b/src/ui/display_utils.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- import os -from typing import Optional, Dict, Any +from typing import Any + from rich.console import Console, Group from rich.panel import Panel from rich.table import Table -from ..utils.config import get_settings, ROOT_DIR # 导入 get_settings 函数 -from ..utils.log_manager import get_module_logger # 导入日志管理器 +from ..utils.config import ROOT_DIR, get_settings # 导入 get_settings 函数 +from ..utils.log_manager import get_module_logger # 导入日志管理器 logger = get_module_logger(__name__) # 获取当前模块的日志器 @@ -25,15 +25,15 @@ def get_relative_path(absolute_path: str) -> str: logger.warning(f"无法将路径 '{absolute_path}' 转换为相对路径,返回原始路径。") return str(absolute_path) -def display_chat_config(console: Console, chat_config: Dict[str, Any]): +def display_chat_config(console: Console, chat_config: dict[str, Any]): """显示从传入的chat_config字典中获取的聊天机器人配置。""" logger.info("正在显示聊天机器人配置。") - def mask_api_key(key: Optional[str]) -> str: + def mask_api_key(key: str | None) -> str: if not key: logger.debug("API Key 未设置,显示为 '[dim]未设置[/dim]'。") return "[dim]未设置[/dim]" - logger.debug("API Key 已设置,显示部分掩码。") - return f"{key[:10]}..." + logger.debug("API Key 已设置,隐藏全部值。") + return "[dim]已设置(值已隐藏)[/dim]" # --- 聊天配置 --- chat_table = Table(title="[bold green]聊天机器人配置[/bold green]", show_header=False, box=None, padding=(0, 1)) From c9ea020d718ec237c1b431e4540866106d5d8f2b Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:22 +0800 Subject: [PATCH 07/35] refactor: modernize retrieval, ETL and script annotations Add type annotations across retrieval, ETL and release scripts, and enforce the release target's OS/CPU architecture in build_binary_release.py so a mismatched --target fails explicitly before any build artifacts are cleaned. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/build_binary_release.py | 62 +++++++++++++-- scripts/embed_knowledge_base.py | 8 +- scripts/extract_release_notes.py | 2 - src/etl/cleaners/base.py | 6 +- src/etl/cleaners/basic_cleaner.py | 8 +- src/etl/extractors/base.py | 6 +- src/etl/extractors/markdown_extractor.py | 8 +- src/etl/pipeline.py | 11 ++- src/etl/splitters/base.py | 6 +- src/etl/splitters/recursive_text_splitter.py | 26 +++--- src/models/document.py | 6 +- src/retrieval/retriever.py | 23 ++++-- src/retrieval/snapshot_repository.py | 19 +++-- src/retrieval/vdb/base.py | 28 +++---- src/retrieval/vdb/factory.py | 25 +++++- src/retrieval/vdb/faiss_store.py | 83 ++++++++++++-------- src/retrieval_test/core.py | 10 +-- src/retrieval_test/excel_logger.py | 21 ++--- 18 files changed, 227 insertions(+), 131 deletions(-) mode change 100644 => 100755 scripts/build_binary_release.py mode change 100644 => 100755 scripts/extract_release_notes.py diff --git a/scripts/build_binary_release.py b/scripts/build_binary_release.py old mode 100644 new mode 100755 index 93d80c5..2979d32 --- a/scripts/build_binary_release.py +++ b/scripts/build_binary_release.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import argparse import os +import platform import shutil -import subprocess + +# The release command is assembled from fixed local build arguments. +import subprocess # nosec B404 import sys import tarfile import tomllib import zipfile from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parent.parent BUILD_ROOT = PROJECT_ROOT / "build" DIST_ROOT = PROJECT_ROOT / "dist" @@ -47,6 +48,54 @@ "_pytest", ] +_TARGET_ENVIRONMENTS = { + "windows-x64": ("windows", "x86_64"), + "macos-x64": ("darwin", "x86_64"), + "macos-arm64": ("darwin", "arm64"), + "linux-x64": ("linux", "x86_64"), + "linux-arm64": ("linux", "arm64"), +} + + +def _normalize_machine(machine: str) -> str: + aliases = { + "amd64": "x86_64", + "x64": "x86_64", + "x86-64": "x86_64", + "aarch64": "arm64", + } + normalized = machine.strip().lower().replace("-", "_") + return aliases.get(normalized, normalized) + + +def validate_target_environment( + target: str, + *, + system: str | None = None, + machine: str | None = None, +) -> None: + """Reject targets that the current PyInstaller host cannot produce. + + PyInstaller builds for the interpreter's host platform and architecture; + the release script does not perform cross-compilation. Failing before the + output cleanup prevents an incorrectly named artifact from replacing a + previously valid one. + """ + expected = _TARGET_ENVIRONMENTS.get(target) + if expected is None: + raise ValueError(f"不支持的发布目标: {target}") + + actual_system = (system or platform.system()).strip().lower() + actual_machine = _normalize_machine(machine or platform.machine()) + expected_system, expected_machine = expected + if actual_system != expected_system or actual_machine != expected_machine: + detected = f"{actual_system}-{actual_machine}" + required = f"{expected_system}-{expected_machine}" + raise RuntimeError( + f"发布目标 {target} 需要 {required} 构建环境,当前检测到 {detected}。" + " PyInstaller 不执行跨平台交叉编译,请在匹配的 runner 或主机上构建。" + ) + def load_version() -> str: with (PROJECT_ROOT / "pyproject.toml").open("rb") as handle: @@ -103,7 +152,8 @@ def run_pyinstaller() -> None: for excluded_module in EXCLUDED_MODULES: command.extend(["--exclude-module", excluded_module]) command.append("main.py") - subprocess.run(command, cwd=PROJECT_ROOT, check=True) + # shell=False and fixed command arguments keep this invocation bounded. + subprocess.run(command, cwd=PROJECT_ROOT, check=True) # nosec B603 def stage_bundle(target: str, version: str) -> Path: @@ -158,7 +208,8 @@ def executable_path(bundle_root: Path) -> Path: def validate_bundle(bundle_root: Path) -> None: executable = executable_path(bundle_root) - result = subprocess.run( + # The executable is the bundle produced immediately before validation. + result = subprocess.run( # nosec B603 [str(executable), "--smoke-test"], cwd=bundle_root, text=True, @@ -174,6 +225,7 @@ def main() -> None: args = parse_args() version = load_version() + validate_target_environment(args.target) clean_output_dirs() run_pyinstaller() bundle_root = stage_bundle(args.target, version) diff --git a/scripts/embed_knowledge_base.py b/scripts/embed_knowledge_base.py index fcde6bc..fcc20c2 100644 --- a/scripts/embed_knowledge_base.py +++ b/scripts/embed_knowledge_base.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- import argparse import asyncio import sys from pathlib import Path -from typing import Optional from rich import box from rich.console import Console @@ -30,12 +28,10 @@ def display_config_and_confirm(splitter_structure_mode: str): settings = get_settings() run_config = build_run_config(settings) - def mask_api_key(key: Optional[str]) -> str: + def mask_api_key(key: str | None) -> str: if not key or key == "no-key-required": return "[dim]未设置或无需设置[/dim]" - if len(key) > 12: - return f"[white]{key[:6]}...{key[-4:]}[/white]" - return "[white]已设置[/white]" + return "[dim]已设置(值已隐藏)[/dim]" table = Table( box=box.ROUNDED, diff --git a/scripts/extract_release_notes.py b/scripts/extract_release_notes.py old mode 100644 new mode 100755 index 27ca185..f682f5f --- a/scripts/extract_release_notes.py +++ b/scripts/extract_release_notes.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import argparse from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parent.parent CHANGELOG_PATH = PROJECT_ROOT / "CHANGELOG.md" diff --git a/src/etl/cleaners/base.py b/src/etl/cleaners/base.py index 38e56b2..6737178 100644 --- a/src/etl/cleaners/base.py +++ b/src/etl/cleaners/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseCleaner(ABC): """ 抽象基类:文档内容清洗器。 @@ -13,7 +14,7 @@ class BaseCleaner(ABC): """ @abstractmethod - def clean(self, documents: List[Document], **kwargs) -> List[Document]: + def clean(self, documents: list[Document], **kwargs) -> list[Document]: """ 对文档列表中的文本内容进行清洗。 @@ -24,4 +25,3 @@ def clean(self, documents: List[Document], **kwargs) -> List[Document]: Returns: List[Document]: 清洗后的文档对象列表。 """ - pass diff --git a/src/etl/cleaners/basic_cleaner.py b/src/etl/cleaners/basic_cleaner.py index e9ac3f3..cd168fc 100644 --- a/src/etl/cleaners/basic_cleaner.py +++ b/src/etl/cleaners/basic_cleaner.py @@ -3,17 +3,19 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 import re -from typing import List -from .base import BaseCleaner + from src.models.document import Document +from .base import BaseCleaner + + class BasicCleaner(BaseCleaner): """ 基础文本内容清洗器。 执行常见的文本清洗操作,如去除多余空格、换行符等。 """ - def clean(self, documents: List[Document], **kwargs) -> List[Document]: + def clean(self, documents: list[Document], **kwargs) -> list[Document]: """ 对文档列表中的文本内容进行基础清洗。 diff --git a/src/etl/extractors/base.py b/src/etl/extractors/base.py index 2c134f1..47d4ca7 100644 --- a/src/etl/extractors/base.py +++ b/src/etl/extractors/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseExtractor(ABC): """ 抽象基类:文档内容抽取器。 @@ -13,7 +14,7 @@ class BaseExtractor(ABC): """ @abstractmethod - def extract(self, document: Document, **kwargs) -> List[Document]: + def extract(self, document: Document, **kwargs) -> list[Document]: """ 从给定的 Document 对象中抽取内容。 对于某些类型(如纯文本),可能只是原样返回。 @@ -26,4 +27,3 @@ def extract(self, document: Document, **kwargs) -> List[Document]: Returns: List[Document]: 抽取出的一个或多个文档对象列表。 """ - pass diff --git a/src/etl/extractors/markdown_extractor.py b/src/etl/extractors/markdown_extractor.py index d329f52..f41f9e3 100644 --- a/src/etl/extractors/markdown_extractor.py +++ b/src/etl/extractors/markdown_extractor.py @@ -2,10 +2,12 @@ # 原始来源: https://github.com/langgenius/dify # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 -from typing import List -from .base import BaseExtractor + from src.models.document import Document +from .base import BaseExtractor + + class MarkdownExtractor(BaseExtractor): """ Markdown 文档内容抽取器。 @@ -13,7 +15,7 @@ class MarkdownExtractor(BaseExtractor): 因为内容已在创建Document对象时从文件中读取。 """ - def extract(self, document: Document, **kwargs) -> List[Document]: + def extract(self, document: Document, **kwargs) -> list[Document]: """ 从代表源文件的 Document 对象中“抽取”内容。 对于Markdown,主要是将单个文档对象放入列表中,以符合流水线格式。 diff --git a/src/etl/pipeline.py b/src/etl/pipeline.py index 602bf78..a880d25 100644 --- a/src/etl/pipeline.py +++ b/src/etl/pipeline.py @@ -3,16 +3,15 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 import time -from typing import List, Type from pathlib import Path -from src.etl.extractors.base import BaseExtractor + from src.etl.cleaners.base import BaseCleaner -from src.etl.splitters.base import BaseSplitter -from src.etl.extractors.markdown_extractor import MarkdownExtractor from src.etl.cleaners.basic_cleaner import BasicCleaner +from src.etl.extractors.base import BaseExtractor +from src.etl.extractors.markdown_extractor import MarkdownExtractor +from src.etl.splitters.base import BaseSplitter from src.etl.splitters.recursive_text_splitter import RecursiveTextSplitter from src.models.document import Document -from src.utils.config import get_settings from src.utils.log_manager import get_module_logger logger = get_module_logger(__name__) @@ -56,7 +55,7 @@ def from_file_path(cls, file_path: Path, splitter_structure_mode: str = "standar splitter=splitter_instance ) - def process(self, document: Document) -> List[Document]: + def process(self, document: Document) -> list[Document]: """ 处理单个文档,并监控每一步的耗时 (CSE Sensor)。 """ diff --git a/src/etl/splitters/base.py b/src/etl/splitters/base.py index 4f1994f..7cf247c 100644 --- a/src/etl/splitters/base.py +++ b/src/etl/splitters/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseSplitter(ABC): """ 抽象基类:文档内容分割器。 @@ -13,7 +14,7 @@ class BaseSplitter(ABC): """ @abstractmethod - def split(self, documents: List[Document], **kwargs) -> List[Document]: + def split(self, documents: list[Document], **kwargs) -> list[Document]: """ 将文档列表中的文本内容分割成更小的块。 @@ -24,4 +25,3 @@ def split(self, documents: List[Document], **kwargs) -> List[Document]: Returns: List[Document]: 分割后的文档块列表。 """ - pass diff --git a/src/etl/splitters/recursive_text_splitter.py b/src/etl/splitters/recursive_text_splitter.py index bcd88ec..f61ac75 100644 --- a/src/etl/splitters/recursive_text_splitter.py +++ b/src/etl/splitters/recursive_text_splitter.py @@ -1,16 +1,16 @@ -# -*- coding: utf-8 -*- import re import uuid -from typing import Callable, List, Optional +from collections.abc import Callable import tiktoken +from langchain_text_splitters import RecursiveCharacterTextSplitter -from .base import BaseSplitter from src.models.document import Document -from langchain_text_splitters import RecursiveCharacterTextSplitter from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from .base import BaseSplitter + logger = get_module_logger(__name__) DEFAULT_CHILD_CHUNK_SIZE = 300 @@ -28,10 +28,10 @@ def __init__( mode: str = "token", encoding_name: str = "cl100k_base", structure_mode: str = "standard", - parent_chunk_size: Optional[int] = None, - parent_chunk_overlap: Optional[int] = None, - child_chunk_size: Optional[int] = None, - child_chunk_overlap: Optional[int] = None, + parent_chunk_size: int | None = None, + parent_chunk_overlap: int | None = None, + child_chunk_size: int | None = None, + child_chunk_overlap: int | None = None, ): """ 初始化 RecursiveTextSplitter。 @@ -101,9 +101,9 @@ def _strip_leading_punctuation(text: str) -> str: """去掉分片开头的句号类标点。""" return re.sub(r"^[\s.。]+", "", text).strip() - def _split_standard_documents(self, documents: List[Document]) -> List[Document]: + def _split_standard_documents(self, documents: list[Document]) -> list[Document]: """标准单层分片。""" - all_chunks: List[Document] = [] + all_chunks: list[Document] = [] for doc in documents: logger.debug(f"正在分割文档: {doc.metadata.get('source', '未知来源')}") @@ -128,9 +128,9 @@ def _split_standard_documents(self, documents: List[Document]) -> List[Document] ) return all_chunks - def split_hierarchical(self, documents: List[Document]) -> List[Document]: + def split_hierarchical(self, documents: list[Document]) -> list[Document]: """层级分片:先切父块,再切子块。""" - all_chunks: List[Document] = [] + all_chunks: list[Document] = [] child_splitter = self._build_child_splitter() self.parent_documents = {} @@ -178,7 +178,7 @@ def split_hierarchical(self, documents: List[Document]) -> List[Document]: return all_chunks - def split(self, documents: List[Document], **kwargs) -> List[Document]: + def split(self, documents: list[Document], **kwargs) -> list[Document]: """ 将文档列表中的文本内容分割成更小的块。 """ diff --git a/src/models/document.py b/src/models/document.py index a851cb7..8ff1c1f 100644 --- a/src/models/document.py +++ b/src/models/document.py @@ -2,8 +2,10 @@ # 原始来源: https://github.com/langgenius/dify # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 +from typing import Any + from pydantic import BaseModel, Field -from typing import Dict, Any + class Document(BaseModel): """ @@ -11,7 +13,7 @@ class Document(BaseModel): 用于在ETL流水线和检索过程中传递文本内容及其相关元数据。 """ content: str - metadata: Dict[str, Any] = Field(default_factory=dict) + metadata: dict[str, Any] = Field(default_factory=dict) def __str__(self): """返回文档内容的字符串表示,方便调试。""" diff --git a/src/retrieval/retriever.py b/src/retrieval/retriever.py index efe34f3..af83278 100644 --- a/src/retrieval/retriever.py +++ b/src/retrieval/retriever.py @@ -6,7 +6,7 @@ from __future__ import annotations import asyncio -from typing import Dict, List +from copy import deepcopy from rich.console import Console @@ -20,6 +20,14 @@ ) from ..utils.config import RetrievalMethod, get_settings +__all__ = [ + "HybridReranker", + "ModelProviderFactory", + "_merge_hybrid_results", + "aretrieve_documents", + "retrieve_documents", +] + def _build_session_config( retrieval_method: RetrievalMethod, @@ -44,8 +52,8 @@ def _build_session_config( score_threshold=score_threshold, active_llm_configuration=settings.default_llm_provider, active_rerank_configuration=active_rerank_configuration, - llm_configurations=settings.llm_configurations, - rerank_configurations=settings.rerank_configurations, + llm_configurations=deepcopy(settings.llm_configurations), + rerank_configurations=deepcopy(settings.rerank_configurations), chat_temperature=settings.chat_temperature, ) return session_config @@ -64,7 +72,7 @@ def retrieve_documents( score_threshold: float, fusion_strategy: str = "rrf", candidate_multiplier: int = 3, -) -> List[Dict]: +) -> list[dict]: return asyncio.run( aretrieve_documents( query=query, @@ -96,7 +104,7 @@ async def aretrieve_documents( score_threshold: float, fusion_strategy: str = "rrf", candidate_multiplier: int = 3, -) -> List[Dict]: +) -> list[dict]: run_config = build_run_config(get_settings()) session_config = _build_session_config( retrieval_method=retrieval_method, @@ -113,4 +121,7 @@ async def aretrieve_documents( vector_store=vector_store, embedding_service=EmbeddingService(run_config), ) - return await retrieval_service.retrieve(query=query, session_config=session_config, console=console) + try: + return await retrieval_service.retrieve(query=query, session_config=session_config, console=console) + finally: + await retrieval_service.aclose() diff --git a/src/retrieval/snapshot_repository.py b/src/retrieval/snapshot_repository.py index 0c30d62..c7ae969 100644 --- a/src/retrieval/snapshot_repository.py +++ b/src/retrieval/snapshot_repository.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import annotations import json @@ -6,7 +5,7 @@ import tomllib import uuid from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any from src.runtime.contracts import KnowledgeSnapshotManifest, RunConfig @@ -17,14 +16,14 @@ def __init__(self, run_config: RunConfig): self.root = run_config.snapshot_root self.root.mkdir(parents=True, exist_ok=True) - def get_active_snapshot_id(self) -> Optional[str]: + def get_active_snapshot_id(self) -> str | None: marker = self.run_config.active_snapshot_marker if not marker.exists(): return None snapshot_id = marker.read_text(encoding="utf-8").strip() return snapshot_id or None - def get_active_snapshot_dir(self) -> Optional[Path]: + def get_active_snapshot_dir(self) -> Path | None: snapshot_id = self.get_active_snapshot_id() if not snapshot_id: return None @@ -38,6 +37,12 @@ def create_temp_snapshot_dir(self, snapshot_id: str) -> Path: temp_dir.mkdir(parents=True, exist_ok=True) return temp_dir + def cleanup_temp_snapshot_dir(self, snapshot_id: str) -> None: + """删除指定构建任务的临时目录,不触碰正式快照。""" + temp_dir = self.root / f".tmp-{snapshot_id}" + if temp_dir.exists(): + shutil.rmtree(temp_dir) + def finalize_snapshot(self, temp_dir: Path, snapshot_id: str) -> Path: final_dir = self.root / snapshot_id if final_dir.exists(): @@ -64,13 +69,13 @@ def load_manifest(self, snapshot_dir: Path) -> KnowledgeSnapshotManifest: data = tomllib.load(file) return KnowledgeSnapshotManifest.from_mapping(data) - def write_stats(self, snapshot_dir: Path, stats: Dict[str, Any]) -> None: + def write_stats(self, snapshot_dir: Path, stats: dict[str, Any]) -> None: (snapshot_dir / "stats.json").write_text( json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8", ) - def load_stats(self, snapshot_dir: Path) -> Dict[str, Any]: + def load_stats(self, snapshot_dir: Path) -> dict[str, Any]: stats_path = snapshot_dir / "stats.json" if not stats_path.exists(): return {} @@ -89,7 +94,7 @@ def validate_snapshot_dir(self, snapshot_dir: Path) -> None: if missing_files: raise FileNotFoundError(f"知识快照不完整,缺少文件: {', '.join(missing_files)}") - def load_active_manifest(self) -> Optional[KnowledgeSnapshotManifest]: + def load_active_manifest(self) -> KnowledgeSnapshotManifest | None: snapshot_dir = self.get_active_snapshot_dir() if snapshot_dir is None: return None diff --git a/src/retrieval/vdb/base.py b/src/retrieval/vdb/base.py index 97a3367..339b404 100644 --- a/src/retrieval/vdb/base.py +++ b/src/retrieval/vdb/base.py @@ -3,7 +3,8 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List, Dict, Any +from typing import Any + class VectorStoreBase(ABC): """ @@ -12,49 +13,42 @@ class VectorStoreBase(ABC): """ @abstractmethod - def add_documents(self, documents: List[Dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): """同步添加文档。""" - pass @abstractmethod - async def aadd_documents(self, documents: List[Dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): """异步添加文档。""" - pass @abstractmethod - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: """同步搜索文档。""" - pass @abstractmethod - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: """异步搜索文档。""" - pass @abstractmethod def save(self, path: str): """保存向量存储。""" - pass @abstractmethod def load(self, path: str): """加载向量存储。""" - pass @abstractmethod def get_embedding_model(self) -> Any: """获取嵌入模型。""" - pass - def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): + def upsert_embeddings(self, documents: list[dict[str, Any]], embeddings: Any): """根据外部已生成的向量写入文档。默认未实现。""" raise NotImplementedError("当前向量存储未实现 upsert_embeddings。") - def semantic_search(self, query_embedding: Any, top_k: int = 5) -> List[Dict[str, Any]]: + def semantic_search(self, query_embedding: Any, top_k: int = 5) -> list[dict[str, Any]]: """使用查询向量执行语义检索。默认未实现。""" raise NotImplementedError("当前向量存储未实现 semantic_search。") - def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any]]: + def keyword_search(self, query_text: str, top_k: int = 5) -> list[dict[str, Any]]: """使用原始文本执行关键词检索。默认退回 search。""" return self.search(query_text, top_k=top_k, search_type="keyword") @@ -66,9 +60,9 @@ def load_snapshot(self, snapshot_dir: str): """从快照目录加载状态。默认未实现。""" raise NotImplementedError("当前向量存储未实现 load_snapshot。") - def register_parent_documents(self, parent_documents: Dict[str, Dict[str, Any]]): + def register_parent_documents(self, parent_documents: dict[str, dict[str, Any]]): """注册父分段侧车数据。""" - return None + return def resolve_parent_content(self, parent_id: str | None) -> str | None: """根据父分段 ID 解析父内容。""" diff --git a/src/retrieval/vdb/factory.py b/src/retrieval/vdb/factory.py index 78bcb3e..0f4a24d 100644 --- a/src/retrieval/vdb/factory.py +++ b/src/retrieval/vdb/factory.py @@ -4,11 +4,11 @@ from pathlib import Path -from .base import VectorStoreBase -from .faiss_store import FaissStore -from ..snapshot_repository import SnapshotRepository from ...runtime.contracts import KnowledgeSnapshotManifest, build_run_config from ...utils.config import get_settings +from ..snapshot_repository import SnapshotRepository +from .base import VectorStoreBase +from .faiss_store import FaissStore class VectorStoreFactory: @@ -33,6 +33,25 @@ def _load_existing_state(store: VectorStoreBase, run_config) -> None: active_snapshot_dir = snapshot_repository.get_active_snapshot_dir() if active_snapshot_dir is not None: snapshot_repository.validate_snapshot_dir(active_snapshot_dir) + manifest = snapshot_repository.load_manifest(active_snapshot_dir) + embedding_detail = run_config.embedding_configurations.get( + run_config.default_embedding_provider + ) + if embedding_detail is None: + raise ValueError( + "当前 embedding 配置缺少活动提供商: " + f"{run_config.default_embedding_provider}" + ) + if ( + manifest.embedding_provider != run_config.default_embedding_provider + or manifest.embedding_model != embedding_detail.model_name + ): + raise RuntimeError( + "活动知识快照的 embedding 配置与当前运行配置不一致: " + f"快照={manifest.embedding_provider}/{manifest.embedding_model}," + f"当前={run_config.default_embedding_provider}/{embedding_detail.model_name}。" + "请重建知识快照或切换回原 embedding 配置。" + ) store.load_snapshot(str(active_snapshot_dir)) return diff --git a/src/retrieval/vdb/faiss_store.py b/src/retrieval/vdb/faiss_store.py index 360b918..e3a0b42 100644 --- a/src/retrieval/vdb/faiss_store.py +++ b/src/retrieval/vdb/faiss_store.py @@ -4,14 +4,17 @@ import json import os -import pickle + +# Pickle is retained only for trusted local legacy snapshots. +import pickle # nosec B403 import time +from copy import deepcopy from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any -import jieba +import jieba # type: ignore[import-untyped] import numpy as np -from rank_bm25 import BM25Okapi +from rank_bm25 import BM25Okapi # type: ignore[import-untyped] try: import faiss @@ -20,31 +23,31 @@ "FAISS is not installed. Please install it with `pip install faiss-cpu`." ) from exc -from .base import VectorStoreBase from ...utils.log_manager import get_module_logger +from .base import VectorStoreBase logger = get_module_logger(__name__) jieba.setLogLevel(jieba.logging.ERROR) class FaissStore(VectorStoreBase): - def __init__(self, file_path: Optional[str] = None): + def __init__(self, file_path: str | None = None): import asyncio self.file_path = file_path - self.documents: List[Dict[str, Any]] = [] - self.embeddings: Optional[np.ndarray] = None - self.parent_documents: Dict[str, Dict[str, Any]] = {} - self._tokenized_docs_cache: List[List[str]] = [] - self.bm25_index: Optional[BM25Okapi] = None - self.faiss_index: Optional[faiss.IndexFlatL2] = None + self.documents: list[dict[str, Any]] = [] + self.embeddings: np.ndarray | None = None + self.parent_documents: dict[str, dict[str, Any]] = {} + self._tokenized_docs_cache: list[list[str]] = [] + self.bm25_index: BM25Okapi | None = None + self.faiss_index: faiss.Index | None = None self.lock = asyncio.Lock() if self.file_path and os.path.exists(self.file_path): self.load(self.file_path) @staticmethod - def _normalize_parent_document(parent_document: Any) -> Dict[str, Any]: + def _normalize_parent_document(parent_document: Any) -> dict[str, Any]: if isinstance(parent_document, dict): return { "content": parent_document.get("content", ""), @@ -55,7 +58,7 @@ def _normalize_parent_document(parent_document: Any) -> Dict[str, Any]: return {"content": "", "metadata": {}} @staticmethod - def _build_index_text(document: Dict[str, Any]) -> str: + def _build_index_text(document: dict[str, Any]) -> str: metadata = document.get("metadata") or {} source = metadata.get("source", "") source_hint = "" @@ -100,7 +103,7 @@ def _normalize_loaded_documents(self) -> None: document["metadata"] = metadata self.parent_documents = normalized_parent_documents - def register_parent_documents(self, parent_documents: Dict[str, Dict[str, Any]]): + def register_parent_documents(self, parent_documents: dict[str, dict[str, Any]]): for parent_id, parent_document in parent_documents.items(): normalized = self._normalize_parent_document(parent_document) if normalized["content"]: @@ -115,7 +118,7 @@ def resolve_parent_content(self, parent_id: str | None) -> str | None: content = parent_document.get("content") return content if isinstance(content, str) and content else None - def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): + def upsert_embeddings(self, documents: list[dict[str, Any]], embeddings: Any): if not documents: return @@ -134,30 +137,39 @@ def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): self.embeddings = np.vstack((self.embeddings, new_embeddings)) self._rebuild_indices() - def add_documents(self, documents: List[Dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): raise RuntimeError("FaissStore.add_documents 已废弃,请使用外部 EmbeddingService 后调用 upsert_embeddings。") - async def aadd_documents(self, documents: List[Dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): raise RuntimeError("FaissStore.aadd_documents 已废弃,请使用 KnowledgeBuildService。") - def semantic_search(self, query_embedding: Any, top_k: int = 5) -> List[Dict[str, Any]]: + def semantic_search(self, query_embedding: Any, top_k: int = 5) -> list[dict[str, Any]]: if self.faiss_index is None or self.embeddings is None: return [] - query_vector = np.array(query_embedding, dtype=np.float32).reshape(1, -1) + query_vector = np.asarray(query_embedding, dtype=np.float32) + if query_vector.ndim != 1 or query_vector.size == 0: + raise ValueError("查询向量必须是一维且非空。") + expected_dimension = int(self.embeddings.shape[1]) + if query_vector.shape[0] != expected_dimension: + raise ValueError( + f"查询向量维度 {query_vector.shape[0]} 与索引维度 {expected_dimension} 不一致;" + "请重建知识快照或切换回构建该快照的嵌入模型。" + ) + query_vector = query_vector.reshape(1, expected_dimension) distances, indices = self.faiss_index.search(query_vector, top_k) similarities = 1.0 / (1.0 + distances[0]) - results: List[Dict[str, Any]] = [] + results: list[dict[str, Any]] = [] for index, doc_index in enumerate(indices[0]): if doc_index == -1: continue - document = pickle.loads(pickle.dumps(self.documents[doc_index])) + document = deepcopy(self.documents[doc_index]) document["score"] = float(similarities[index]) results.append(document) return results - def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any]]: + def keyword_search(self, query_text: str, top_k: int = 5) -> list[dict[str, Any]]: if self.bm25_index is None: return [] @@ -165,22 +177,22 @@ def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any] doc_scores = self.bm25_index.get_scores(tokenized_query) top_indices = np.argsort(doc_scores)[::-1] - results: List[Dict[str, Any]] = [] + results: list[dict[str, Any]] = [] for index in top_indices: score = float(doc_scores[index]) if score <= 0: break - document = pickle.loads(pickle.dumps(self.documents[index])) + document = deepcopy(self.documents[index]) document["score"] = score results.append(document) if len(results) >= top_k: break return results - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: raise RuntimeError("FaissStore.search 已废弃,请通过 RetrievalService 调用语义检索或关键词检索。") - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: raise RuntimeError("FaissStore.asearch 已废弃,请通过 RetrievalService 调用语义检索或关键词检索。") def save(self, path: str): @@ -197,8 +209,10 @@ def save(self, path: str): def load(self, path: str): if not os.path.exists(path): raise FileNotFoundError(f"向量存储文件未找到: {path}") + # Legacy compatibility: this path is only for local files generated by + # PyRAG-Kit. Never point it at an untrusted pickle file. with open(path, "rb") as file: - data = pickle.load(file) + data = pickle.load(file) # nosec B301 self.documents = data.get("documents", []) self.embeddings = data.get("embeddings") self.parent_documents = data.get("parent_documents", {}) @@ -206,6 +220,8 @@ def load(self, path: str): self._rebuild_indices() def save_snapshot(self, snapshot_dir: str): + if self.faiss_index is None or self.embeddings is None: + raise RuntimeError("当前语义索引为空,无法保存快照。") snapshot_path = Path(snapshot_dir) snapshot_path.mkdir(parents=True, exist_ok=True) with (snapshot_path / "chunks.pkl").open("wb") as file: @@ -215,8 +231,6 @@ def save_snapshot(self, snapshot_dir: str): with (snapshot_path / "lexical.index").open("wb") as file: pickle.dump(self._tokenized_docs_cache, file) np.save(snapshot_path / "embeddings.npy", self.embeddings) - if self.faiss_index is None: - raise RuntimeError("当前语义索引为空,无法保存快照。") faiss.write_index(self.faiss_index, str(snapshot_path / "semantic.index")) stats = { "document_count": len({doc.get("metadata", {}).get("source") for doc in self.documents}), @@ -231,10 +245,13 @@ def save_snapshot(self, snapshot_dir: str): def load_snapshot(self, snapshot_dir: str): snapshot_path = Path(snapshot_dir) + # SnapshotRepository accepts only application-managed snapshot dirs; + # keep the pickle extension for backward compatibility with existing + # Dify-derived snapshots and do not load arbitrary external files. with (snapshot_path / "chunks.pkl").open("rb") as file: - self.documents = pickle.load(file) + self.documents = pickle.load(file) # nosec B301 with (snapshot_path / "parents.pkl").open("rb") as file: - self.parent_documents = pickle.load(file) + self.parent_documents = pickle.load(file) # nosec B301 embeddings_path = snapshot_path / "embeddings.npy" self.embeddings = np.load(embeddings_path) if embeddings_path.exists() else None faiss_index_path = snapshot_path / "semantic.index" @@ -242,7 +259,7 @@ def load_snapshot(self, snapshot_dir: str): lexical_path = snapshot_path / "lexical.index" if lexical_path.exists(): with lexical_path.open("rb") as file: - self._tokenized_docs_cache = pickle.load(file) + self._tokenized_docs_cache = pickle.load(file) # nosec B301 self.bm25_index = BM25Okapi(self._tokenized_docs_cache) else: self._rebuild_indices() diff --git a/src/retrieval_test/core.py b/src/retrieval_test/core.py index 40a4b54..f7c2c11 100644 --- a/src/retrieval_test/core.py +++ b/src/retrieval_test/core.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- import asyncio import os -from typing import Dict, List from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -24,7 +22,7 @@ logger = get_module_logger(__name__) -def display_results(query: str, documents: List[Dict]): +def display_results(query: str, documents: list[dict]): if not documents: console.print(Panel(f"对查询 “[bold yellow]{query}[/bold yellow]” 无结果。", border_style="red")) return @@ -50,7 +48,7 @@ async def run_retrieval_test_async(): excel_logger = None try: excel_logger = ExcelLogger() - except Exception as exc: + except Exception as exc: # noqa: BLE001 - interactive setup reports all failures logger.error("ExcelLogger 初始化失败: %s", exc) try: @@ -58,7 +56,7 @@ async def run_retrieval_test_async(): session_config = build_session_config(get_settings()) vector_store = VectorStoreFactory.get_default_vector_store() retrieval_service = RetrievalService(vector_store, EmbeddingService(run_config)) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - interactive setup reports all failures console.print(f"[red]数据库加载失败: {exc}[/red]") return @@ -77,7 +75,7 @@ async def run_retrieval_test_async(): excel_logger.log_results(query, documents) except (KeyboardInterrupt, EOFError): break - except Exception as exc: + except Exception as exc: # noqa: BLE001 - interactive loop remains usable console.print(f"[red]召回测试出错: {exc}[/red]") logger.error("召回测试出错: %s", exc) diff --git a/src/retrieval_test/excel_logger.py b/src/retrieval_test/excel_logger.py index 8ddb87b..3be8971 100644 --- a/src/retrieval_test/excel_logger.py +++ b/src/retrieval_test/excel_logger.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -import os import json +import os from datetime import datetime -from typing import List, Dict, Any -from openpyxl import Workbook -from openpyxl.worksheet.worksheet import Worksheet +from typing import Any + +from openpyxl import Workbook # type: ignore[import-untyped] +from openpyxl.worksheet.worksheet import Worksheet # type: ignore[import-untyped] from ..utils.log_manager import get_module_logger @@ -25,7 +25,7 @@ def __init__(self, log_dir: str = "data/logs"): os.makedirs(log_dir, exist_ok=True) # 生成带时间戳的唯一文件名 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + timestamp = datetime.now().astimezone().strftime("%Y%m%d_%H%M%S") self.filepath = os.path.join(log_dir, f"recall_test_log_{timestamp}.xlsx") # 创建一个新的 Excel 工作簿和工作表 @@ -53,7 +53,7 @@ def _write_header(self): self.worksheet.append(headers) self.workbook.save(self.filepath) - def log_results(self, query: str, results: List[Dict[str, Any]]): + def log_results(self, query: str, results: list[dict[str, Any]]): """ 将单次查询的结果记录到 Excel 文件中。 @@ -61,8 +61,9 @@ def log_results(self, query: str, results: List[Dict[str, Any]]): query (str):用户的查询。 results (List[Dict[str, Any]]): 从检索器返回的文档列表。 """ - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S") + row: list[Any] if not results: # 如果没有结果,也记录一条信息 row = [timestamp, query, "N/A", "N/A", "无结果", "N/A", "N/A", "N/A"] @@ -100,5 +101,5 @@ def log_results(self, query: str, results: List[Dict[str, Any]]): try: self.workbook.save(self.filepath) logger.debug(f"成功将查询 '{query}' 的 {len(results)} 条结果记录到 {self.filepath}") - except Exception as e: - logger.error(f"保存 Excel 日志文件失败: {e}", exc_info=True) + except Exception: + logger.exception("保存 Excel 日志文件失败") From bd877aa0951913628196f4ac35ec68e6e12636a3 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:23 +0800 Subject: [PATCH 08/35] docs: sync guides, example config and changelog Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 14 +++-- AGENTS.md | 74 ++++++++++++++++------ CHANGELOG.md | 33 ++++++++++ README.md | 4 +- config.toml.example | 42 +++++++------ docs/developer_docs/developer-guide.md | 16 +++-- docs/user_guide/configuration.md | 21 ++++++- docs/user_guide/getting-started.md | 4 +- docs/user_guide/llm-providers.md | 85 ++++++++++++++++++++++++-- docs/user_guide/user-guide.md | 2 +- 10 files changed, 235 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index 976335e..69e6e7f 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,17 @@ DEFAULT_LLM_PROVIDER="openai" # 其他可选模型密钥 ANTHROPIC_API_KEY="" GOOGLE_API_KEY="" +# google-genai 官方别名;同时设置时 GOOGLE_API_KEY 优先 +# GEMINI_API_KEY="" +# Vertex AI ADC(不使用 GOOGLE_API_KEY 时启用) +# GOOGLE_GENAI_USE_VERTEXAI="true" +# GOOGLE_CLOUD_PROJECT="your-project-id" +# GOOGLE_CLOUD_LOCATION="us-central1" +# GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" SILICONFLOW_API_KEY="" QWEN_API_KEY="" +# Ark API key(与 AccessKey/SecretKey 二选一) +ARK_API_KEY="" VOLC_ACCESS_KEY="" VOLC_SECRET_KEY="" JINA_API_KEY="" @@ -21,8 +30,3 @@ GROK_API_KEY="" # 默认嵌入模型已在 config.toml 中设置为 local-hash。 # 如需切换,请在这里覆盖 DEFAULT_EMBEDDING_PROVIDER 或在 config.toml 中修改。 # DEFAULT_EMBEDDING_PROVIDER="local-hash" - -# iflow 示例 -# OPENAI_API_KEY="sk-your-iflow-key" -# OPENAI_API_BASE="https://apis.iflow.cn/v1" -# DEFAULT_LLM_PROVIDER="iflow-qwen3-max" diff --git a/AGENTS.md b/AGENTS.md index fc3942d..5db3a3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,32 +1,66 @@ -# 仓库指南 (Repository Guidelines) +# Repository Guidelines -## 项目结构与模块组织 (Project Structure & Module Organization) +本指南适用于仓库根目录的 PyRAG-Kit。项目要求 Python 3.11 或更高版本,使用 `uv` 管理依赖。 -本仓库是一个 Python 3.11+ 的 RAG 工具集。核心代码位于 `src/` 目录下,按职责拆分为以下主要模块: -`src/chat/`、`src/providers/`、`src/retrieval/`、`src/etl/`、`src/ui/` 和 `src/utils/`。入口程序和脚本位于 -`main.py` 和 `scripts/`。测试代码位于 `tests/`,其目录结构与源码对应。运行产物(如 `data/` 目录和生成的日志)不应提交至版本控制。 +## Project Structure & Module Organization -## 构建、测试与开发命令 (Build, Test, and Development Commands) +- `main.py` 是交互式入口;`src/` 按职责划分为 `chat/`、`etl/`、`models/`、`providers/`、`retrieval/`、`retrieval_test/`、`runtime/`、`services/`、`ui/` 和 `utils/`。 +- `scripts/` 放置知识库快照构建、发布打包和发布说明提取脚本;`tests/` 按源码模块组织 pytest 测试。 +- `knowledge_base/` 保存本地 Markdown 原文;`data/kb/` 和 `data/logs/` 保存运行产物,均不应提交。仓库不再包含 Dify 上游子模块,Dify 相关逻辑以 `src/` 中的移植代码维护。 +- 核心流程为 ETL -> embedding -> FAISS 快照 -> 混合检索 -> provider-backed chat。 -- `uv sync`:根据 `pyproject.toml` 安装依赖并创建本地环境。 -- `uv run main.py`:启动应用程序。 -- `uv run python -m scripts.embed_knowledge_base`:重建知识库快照。 -- `uv run pytest`:运行完整测试套件。 -- `uv run pytest tests/test_config.py`:在迭代特定更改时运行单个测试文件。 +## Build, Test, and Development Commands -## 编码风格与命名规范 (Coding Style & Naming Conventions) +```bash +uv sync +uv sync --group dev +uv run main.py --smoke-test +uv run main.py +uv run python -m scripts.embed_knowledge_base --mode standard +uv run pytest +uv run pytest tests/providers/test_factory.py +uv run python scripts/build_binary_release.py --target macos-arm64 --validate +``` -遵循现有的 Python 风格:使用 4 空格缩进,公开函数必须包含明确的类型标注,命名应具有描述性。 -函数、变量和模块使用 `snake_case`;类使用 `PascalCase`;常量使用 `UPPER_CASE`。保持文档字符串(docstrings)和注释简短且客观。修改代码时应贴合周围代码风格,避免引入新的格式化样式。 +`standard` 也可替换为 `hierarchical`。发布目标支持 `windows-x64`、`macos-x64`、`macos-arm64`、`linux-x64` 和 `linux-arm64`;标签 `v*` 会触发 GitHub Release 工作流。 +发布脚本不做跨平台交叉编译,`--target` 必须匹配当前主机或 CI runner 的系统与 CPU 架构;目标不匹配时会在清理构建产物前显式失败。 -## 测试指南 (Testing Guidelines) +## Quality Checks -所有自动化测试统一使用 `pytest`。测试文件命名为 `test_*.py`,测试函数命名为 `test_*`。建议将针对特定行为的测试放在该行为相关的代码附近,特别是对于配置解析、提供商工厂、ETL 和检索逻辑。项目没有强制的覆盖率阈值;针对每次行为变更都应补充测试,并优先运行最小相关测试集。 +开发依赖包含 Ruff、Bandit 和 MyPy。提交前可运行: -## 提交与拉取请求指引 (Commit & Pull Request Guidelines) +```bash +uv run ruff check main.py src scripts tests +uv run bandit -r main.py src scripts -ll +uv run mypy --cache-dir /tmp/pyrag-kit-mypy main.py src +uv run python -m compileall -q main.py src scripts tests +uv lock --check +git diff --check +``` -Git 提交历史应使用约定式前缀,如 `feat:`、`refactor:`、`test:` 和 `chore:`,后接简短摘要。提交信息应具体且使用祈使句。拉取请求(PR)需说明改动内容,列出验证命令,并注明是否对配置或数据文件产生影响。仅在 UI 输出发生变化时附带截图。 +MyPy 使用任务专用缓存目录,避免多个进程共享 `.mypy_cache` 造成锁等待;优先修复本次改动引入的问题,并记录未覆盖的历史基线告警。 -## 安全与配置提示 (Security & Configuration Tips) +## Coding Style & Naming Conventions -严禁提交密钥或本地覆盖配置。请复制 `config.toml.example` 为本地 `config.toml`,并使用 `.env` 文件处理私密数值。`config.toml` 为本地文件,默认不纳入版本控制。将 `data/` 目录和生成的日志视为本地产物。若在 `dify/` 子目录下工作,请先阅读该目录下的 `AGENTS.md`,因为子目录指令优先于此根目录指南。 +使用 4 个空格缩进、明确的公开函数类型标注和简短客观的 docstring。函数、变量和模块使用 `snake_case`,类使用 `PascalCase`,常量使用 `UPPER_CASE`。项目使用 Ruff 做 lint 检查,未配置独立 formatter;修改时遵循相邻代码风格并运行 `git diff --check`。保持服务、provider、检索和快照边界,不用静默回退或占位结果掩盖失败。 + +## Testing Guidelines + +测试框架为 pytest,配置见 `pytest.ini`;该配置禁用未使用的 `langsmith` 插件。测试文件命名为 `test_*.py`,测试函数命名为 `test_*`。新增行为应在对应 `tests/` 子目录补充回归测试;优先运行相关文件,再运行完整 `uv run pytest`。仓库未设置强制覆盖率阈值。 + +## Commit & Pull Request Guidelines + +提交历史使用 `feat:`、`fix:`、`docs:`、`build:`、`ci:`、`chore:` 等约定式前缀,并以简短祈使句说明变更。PR 应说明目的、影响范围和验证命令,注明配置或生成数据影响;只有 UI 输出改变时才附截图。发布相关变更同步更新 `CHANGELOG.md`。 + +## Security & Configuration Tips + +用 `cp config.toml.example config.toml` 和 `cp .env.example .env` 创建本地配置。密钥只放环境变量或本地 `.env`,不得写入源码、示例、日志或提交。根项目框架采用 MIT;`src/etl/`、`src/retrieval/` 等 Dify 衍生部分仍须遵守 `DIFY_LICENSE`,并保留上游版权声明。移除上游子模块不改变这些许可义务。 +知识快照保留 Dify 兼容的 pickle 文件格式;只从本项目生成的本地快照或明确受信的 legacy 文件加载,不导入不可信来源的 `.pkl` 文件。 + +## API Channel Integration + +Provider 必须在 `src/providers/factory.py` 声明能力并通过对应抽象接口校验。OpenAI 兼容渠道复用 `openai` SDK;Google、Anthropic、Volcengine 使用各自 SDK;Jina 和 SiliconFlow Rerank 使用 HTTP JSON。新增或调整渠道时同步更新 `config.toml.example`、用户文档和 provider 契约测试。Google 的 Auth Token、Anthropic Beta 资源和 Ark Classification 资源都需通过显式 Facade/入口调用,不能把缺少资源挂载静默当成成功。 + +`local-hash` 是无需远端凭证的离线 Embedding provider,默认配置使用它构建本地知识快照。 + +LLM 配置可通过 `protocol = "responses"` 选择 OpenAI/Ark Responses;省略时保持 `chat_completions` 兼容行为。Responses 仅在目标 Base URL 确实提供 `/responses` 时可用,`protocol_status()` 的 `adapter_supported` 不等于真实服务端验收;兼容渠道和 Ark 都必须在 `options.server_verified_protocols` 中显式登记已验证协议。Ark Responses 可使用 SDK 支持的 `input_audio`、`input_video` 和 `image_pixel_limit`,OpenAI 兼容适配器不得把这些 Ark 专属块静默发送。Embedding 和 Rerank 配置不得填写 `protocol`。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0ce8e..524f668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ 所有此项目的显著更改都将记录在此文件中。 +## [1.4.0] - 2026-09-20 + +### Provider 与协议 + +- 建立统一 Provider 工厂与抽象请求模型,补齐 LLM、Embedding、Rerank 的能力声明、配置校验、缓存与同步/异步生命周期管理。 +- 新增 `complete()`/`acomplete()` 高级调用入口,统一支持多轮消息、工具调用、结构化输出、多模态内容、usage 与结束原因;`invoke()`/`ainvoke()` 保留文本流兼容行为。 +- 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses。 +- Responses 对非官方端点要求 `options.server_verified_protocols = ["responses"]` 显式登记;未登记时工厂、Provider 和资源 Facade 均在请求前拒绝,避免把本地 SDK 资源误发到未实现 `/responses` 的服务端。 +- 新增原生资源 Facade,显式暴露文件、批处理、缓存、向量库、token 计数、调优等 SDK 能力,不再把资源生命周期混入普通聊天请求。 +- 深度适配 Google GenAI、Anthropic、Volcengine Ark、Jina 与 SiliconFlow Rerank;OpenAI 兼容渠道复用 `openai` SDK,Jina 与 SiliconFlow Rerank 使用 HTTP JSON。 +- Embedding 区分文档与查询任务类型:Google 使用 `RETRIEVAL_DOCUMENT` 与 `RETRIEVAL_QUERY`。 + +### 安全 + +- 新增配置、请求与日志边界的凭证校验:模型级 `options` 与请求级扩展不得携带密钥、请求头、query 或 Base URL 覆盖,资源 Facade 的关键字与位置参数同样受检。 +- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query)。 +- 兼容渠道不再被当作官方 OpenAI 端点放行 SDK 专属资源;仅精确匹配 `https://api.openai.com/v1` 时按 SDK 契约放行。 +- 收紧失败语义:凭证、SDK 或远端调用失败时显式报错,不再以空 embedding、零分 rerank 或占位回答掩盖失败。 + +### 移除与调整 + +- 移除 Dify 上游子模块;`src/etl/`、`src/retrieval/` 等移植代码继续遵守 `DIFY_LICENSE` 并保留上游版权声明。 +- 移除 iFlow 渠道及其配置示例。 +- 移除 qwen rerank 配置示例;该渠道不提供 rerank 能力,调用时会显式报错。 +- 发布脚本新增目标环境校验,`--target` 与主机系统/架构不匹配时在清理构建产物前显式失败,不再生成错误架构的发布包。 +- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。 + +### 测试与文档 + +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 760。 +- 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 +- 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 + ## [1.3.0] - 2026-03-20 ### 运行与配置 diff --git a/README.md b/README.md index 7352db2..544c07c 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,9 @@ uv sync --group dev uv run python scripts/build_binary_release.py --target macos-x64 --validate ``` -发布工作流定义在 [`.github/workflows/release.yml`](/Volumes/Work/code/PyRAG-kit/.github/workflows/release.yml),发布说明会从 [CHANGELOG.md](/Volumes/Work/code/PyRAG-kit/CHANGELOG.md) 的对应版本节自动提取。 +打包脚本不执行跨平台交叉编译,`--target` 必须与当前主机或 CI runner 的系统和 CPU 架构一致;例如 Intel macOS 使用 `macos-x64`,Apple Silicon macOS 使用 `macos-arm64`。不匹配时脚本会在清理产物前显式失败,避免生成错误架构的发布包。 + +发布工作流定义在 [`.github/workflows/release.yml`](./.github/workflows/release.yml),发布说明会从 [CHANGELOG.md](./CHANGELOG.md) 的对应版本节自动提取。 二进制包内默认只包含空的 `knowledge_base/` 占位目录,使用时请自行放入 Markdown 源文档后再构建知识快照。 --- diff --git a/config.toml.example b/config.toml.example index 67cecc8..25fb39a 100644 --- a/config.toml.example +++ b/config.toml.example @@ -12,16 +12,16 @@ openai_api_base = "https://api.openai.com/v1" # SiliconFlow 的 Base URL siliconflow_base_url = "https://api.siliconflow.cn/v1" -# Qwen 官方接口的 Base URL -qwen_base_url = "https://dashscope.aliyuncs.com/api/v1" +# Qwen OpenAI 兼容接口的 Base URL +qwen_base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" # DeepSeek 的 Base URL deepseek_base_url = "https://api.deepseek.com" # Ollama 的 Base URL ollama_base_url = "http://localhost:11434/v1" # LM Studio 的 Base URL lm_studio_base_url = "http://localhost:1234/v1" -# 火山引擎的 Base URL -volc_base_url = "https://maas-api.ml-platform-cn-beijing.volces.com" +# 火山引擎 Ark API 的 Base URL +volc_base_url = "https://ark.cn-beijing.volces.com/api/v3" # Grok 的 Base URL grok_base_url = "https://api.x.ai/v1" @@ -104,6 +104,8 @@ model_name = "local-hash-256" provider = "google" # API 实际模型名 model_name = "models/text-embedding-004" +# 可选 SDK 参数:查询会自动使用 RETRIEVAL_QUERY,文档使用 RETRIEVAL_DOCUMENT +# options = { output_dimensionality = 768 } # openai 向量化模型 [embedding_configurations.openai] @@ -155,13 +157,6 @@ provider = "jina" # API 实际模型名 model_name = "jina-reranker-v2-base-multilingual" -# qwen rerank 模型 -[rerank_configurations.qwen] -# 提供商实现标识 -provider = "qwen" -# API 实际模型名 -model_name = "Qwen/Qwen3-Reranker-4B" - # --- LLM 配置 --- # google 聊天模型 [llm_configurations.google] @@ -177,19 +172,22 @@ provider = "google" # API 实际模型名 model_name = "gemini-2.5-pro" -# openai-gpt4o 聊天模型 -[llm_configurations.openai-gpt4o] +# openai 聊天模型;与 .env.example 的 DEFAULT_LLM_PROVIDER=openai 对应 +[llm_configurations.openai] # 提供商实现标识 provider = "openai" # API 实际模型名 model_name = "gpt-4o" -# iflow-qwen3-max 聊天模型 -[llm_configurations.iflow-qwen3-max] -# 提供商实现标识 +# openai-responses Responses 协议示例 +# 仅当 Base URL 对应服务端实现了 /responses 时使用;省略 protocol 默认使用 Chat Completions。 +[llm_configurations.openai-responses] provider = "openai" -# API 实际模型名 -model_name = "qwen3-max" +model_name = "gpt-5" +protocol = "responses" +# 仅放非敏感 SDK 参数;API key、Base URL 必须放 .env/环境变量 +# 如果 Base URL 是代理、中转或自建网关,完成服务端验证后显式登记: +# options = { server_verified_protocols = ["responses"], reasoning = { effort = "medium" }, store = true } # openai-gpt3.5 聊天模型 [llm_configurations."openai-gpt3.5"] @@ -219,6 +217,14 @@ provider = "volcengine" # API 实际模型名 model_name = "doubao-pro-32k" +# Ark Responses 示例;只有目标 Ark 服务端支持 /responses 时启用 +# [llm_configurations.volc-responses] +# provider = "volcengine" +# model_name = "doubao-seed-1-6-251015" +# protocol = "responses" +# 仅在目标 Ark Base URL 已实际验证 /responses 后保留此登记 +# options = { server_verified_protocols = ["responses"] } + # sf-qwen2-7b 聊天模型 [llm_configurations.sf-qwen2-7b] # 提供商实现标识 diff --git a/docs/developer_docs/developer-guide.md b/docs/developer_docs/developer-guide.md index 48b98a0..c443143 100644 --- a/docs/developer_docs/developer-guide.md +++ b/docs/developer_docs/developer-guide.md @@ -49,17 +49,23 @@ PyRAG-Kit 采用了清晰、模块化的项目结构,旨在实现高内聚、 这是扩展新模型的关键目录。 -* **`__base__/model_provider.py`**: 定义了所有模型提供商必须遵循的抽象基类 `LargeLanguageModel` 和 `RerankModel`。它们规定了 `invoke` 和 `rerank` 等核心接口。 +* **`__base__/model_provider.py`**: 定义 `CompletionRequest`、`CompletionResult`、`LargeLanguageModel`、`TextEmbeddingModel` 和 `RerankModel`。`invoke`/`ainvoke` 保留文本流兼容接口;需要多轮、多模态、工具调用、结构化输出和 usage 时使用 `complete`/`acomplete`。 * **`factory.py`**: 实现了一个工厂模式,用于根据配置动态创建和获取指定的模型提供商实例。 * **`google.py`, `openai.py`, etc.**: 每个文件都是一个具体模型提供商的实现,负责处理与该平台 API 的所有交互(如认证、请求构建、响应解析)。 **如何添加一个新的模型提供商?** 1. 在 `src/providers/` 目录下创建一个新的 `my_new_provider.py` 文件。 -2. 在该文件中创建一个类,继承自 `LargeLanguageModel`。 -3. 实现基类中定义的抽象方法,主要是 `invoke()`。 -4. 在 `src/providers/factory.py` 中导入您的新类,并在 `ModelProviderFactory` 的 `get_llm_provider` 方法中添加一个 `elif` 分支来注册您的新提供商。 -5. 在 `config.toml` 的 `llm_configurations`、`embedding_configurations` 或 `rerank_configurations` 中添加使用您的新提供商的配置。 +2. 在该文件中实现所需能力接口:`LargeLanguageModel`、`TextEmbeddingModel` 或 `RerankModel`。 +3. 实现对应抽象方法,并在 `src/providers/factory.py` 的 `_provider_map` 中注册模块和类;不要在 `get_*_provider` 方法中添加分支。 +4. 确保配置角色与实现能力匹配。工厂会在实例化时校验接口,并对需要不同协议实现的渠道使用角色别名。 +5. 在 `config.toml` 的 `llm_configurations`、`embedding_configurations` 或 `rerank_configurations` 中添加配置,并为工厂和渠道请求补充测试。 + +Provider 的协议边界保持清晰:OpenAI、Qwen、SiliconFlow、DeepSeek、Grok、Ollama 和 LM Studio 复用 `openai` SDK 的 Chat/Responses 兼容接口;这些渠道的 LLM 配置默认使用 Chat Completions,也可显式选择 `responses`,但兼容服务端是否实现该路径必须单独验证。官方 OpenAI Base URL 可按 SDK 契约使用 Responses;代理、中转或自建 Base URL 必须在完成实际验证后通过 `options.server_verified_protocols = ["responses"]` 登记,否则 Provider 会在请求前显式拒绝。只有官方 OpenAI Provider 声明 Files、Batches、Vector Stores、Skills、Realtime、Webhooks、Admin 和 Content Provenance Checks 等 OpenAI 专属资源,并通过 `.native` 保留当前 SDK 的完整资源树。Google、Anthropic 和 Volcengine 使用各自 SDK,Volcengine 同时支持 Ark Chat、Responses 和 Classification;Ark Classification 通过 SDK 官方资源类显式挂载,SDK 不提供该资源时直接报错。Jina 与 SiliconFlow Rerank 使用 HTTP JSON 接口。新增渠道时先确认协议、服务端证据和能力,再选择复用适配器或实现独立 provider。 + +`ModelDetail.options` 是非敏感 SDK 扩展参数的唯一配置入口。工厂会将其传入 Provider,并拒绝凭证、请求头和 Base URL 字段;Provider 还会把客户端构造参数与请求参数分流,不支持的能力应在 Provider 边界显式报错。SDK 专属文件、批处理、缓存、token 计数等资源通过 Provider 的显式方法调用,不得塞进普通 `invoke()`。同步和异步 Facade 应保持 SDK 的真实调用约定:例如 Google `aio.models.generate_content_stream()` 需要等待一次后再异步迭代,而 `aio.chats.create()` 本身同步返回 `AsyncChat`。 + +Responses 适配位于 `src/providers/openai_compatible.py`,共享消息归一化位于 `src/providers/__base__/model_provider.py`。请求构造、Chat Completions 工具转换、`assistant.tool_calls`/`role=tool` 历史转换、`text`/`image_url`/`file` 多模态块转换、同步/异步文本提取和 SSE 事件解析集中在这些适配器中;Ark Responses 还要按目标 SDK 变体转换 `input_audio`、`input_video` 和 `image_pixel_limit`,当前 OpenAI SDK 不支持的音视频输入块必须在请求前显式拒绝。`response.failed`、`response.incomplete`、取消状态和错误事件必须显式转换为异常,不能静默结束流。扩展事件类型时应先补契约测试,再修改统一文本流接口。 **当前默认嵌入提供商** diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index 82f1849..c7156d5 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -44,18 +44,33 @@ cp .env.example .env ``` ```dotenv -OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxx" +OPENAI_API_KEY="your-openai-api-key" GOOGLE_API_KEY="AIza..." +# 也可使用 google-genai 官方别名;同时设置时 GOOGLE_API_KEY 优先 +# GEMINI_API_KEY="AIza..." ``` `.env` 中的同名键会覆盖 `config.toml`。 +Google Vertex AI 使用 Application Default Credentials(ADC)时,不要把凭证对象或密钥内容写入 `config.toml`。 +请在运行环境中完成 `gcloud auth application-default login`,或设置服务账号文件路径,并显式选择 Vertex 模式: + +```dotenv +GOOGLE_GENAI_USE_VERTEXAI="true" +GOOGLE_CLOUD_PROJECT="your-project-id" +GOOGLE_CLOUD_LOCATION="us-central1" +# 可选:改用服务账号文件;文件本身不要提交到仓库 +GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" +``` + +没有 `GOOGLE_API_KEY` 时,`GOOGLE_GENAI_USE_VERTEXAI=true` 会让 Provider 使用 Vertex ADC;`ModelDetail.options` 只接受非敏感参数,不能用来保存 `credentials`。 + 如果您使用 OpenAI 兼容渠道,可以这样配置: ```dotenv OPENAI_API_KEY="sk-..." -OPENAI_API_BASE="https://apis.iflow.cn/v1" -DEFAULT_LLM_PROVIDER="iflow-qwen3-max" +OPENAI_API_BASE="https://api.openai.com/v1" +DEFAULT_LLM_PROVIDER="openai" ``` ## 主要字段 diff --git a/docs/user_guide/getting-started.md b/docs/user_guide/getting-started.md index adeb37b..926a55c 100644 --- a/docs/user_guide/getting-started.md +++ b/docs/user_guide/getting-started.md @@ -62,8 +62,8 @@ uv sync 4. 打开 `.env` 文件,填入您要使用的聊天模型 API Key。例如,如果您使用 OpenAI 兼容渠道: ```dotenv OPENAI_API_KEY="sk-..." - OPENAI_API_BASE="https://apis.iflow.cn/v1" - DEFAULT_LLM_PROVIDER="iflow-qwen3-max" + OPENAI_API_BASE="https://api.openai.com/v1" + DEFAULT_LLM_PROVIDER="openai" ``` 5. 如需调整默认路径、base url 或检索参数,请编辑 `config.toml`。 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index eb8cdad..5ceea14 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -37,7 +37,81 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 如果您使用代理、第三方中转服务或本地模型,请在 `config.toml` 中配置对应的 API 访问地址。例如,Ollama 的默认地址是 `http://localhost:11434/v1`。 -### 2. 定义模型实例 +Google Vertex AI 的 Application Default Credentials(ADC)通过环境变量配置,不要把凭证对象或密钥内容写入模型条目的 `options`。先完成 ADC 登录,再设置: + +```dotenv +GOOGLE_GENAI_USE_VERTEXAI="true" +GOOGLE_CLOUD_PROJECT="your-project-id" +GOOGLE_CLOUD_LOCATION="us-central1" +# 可选:服务账号文件路径(不要提交该文件) +GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" +``` + +`google-genai` 也支持 `GEMINI_API_KEY` 作为 API key 别名;同时设置时优先使用 `GOOGLE_API_KEY`。未设置 API key 时,显式的 Vertex 开关允许 SDK 使用 ADC;程序化集成也可向 `GoogleProvider` 传入 `credentials`,但该对象不能放进 `config.toml`。 + +### 2. 协议与 SDK + +不同渠道不强制使用各供应商的官方 SDK,项目按协议复用适配器: + +| 渠道 | 能力 | 实现方式 | +| --- | --- | --- | +| Google | LLM、Embedding | `google-genai` SDK | +| OpenAI | LLM、Embedding | `openai` SDK | +| Qwen、SiliconFlow、Ollama、LM Studio | LLM、Embedding | `openai` SDK 的 Chat/Responses 兼容接口;资源能力按目标服务端另行确认 | +| DeepSeek、Grok | LLM | `openai` SDK 的 Chat/Responses 兼容接口;资源能力按目标服务端另行确认 | +| Anthropic | LLM | `anthropic` SDK | +| Volcengine | LLM、Embedding、Ark Responses | Volcengine Ark SDK | +| Jina、SiliconFlow | Rerank | HTTP JSON 接口(`httpx`) | + +Qwen 的 OpenAI 兼容地址默认使用 `https://dashscope.aliyuncs.com/compatible-mode/v1`;SiliconFlow 的聊天/Embedding 和 Rerank 是两种不同接口,Rerank 会由工厂映射到专用适配器。 + +OpenAI 兼容的 LLM 配置默认调用 Chat Completions。需要调用 Responses 时,在单个 `llm_configurations` 条目中设置 `protocol = "responses"`。Ark 也可使用同样的协议字段: + +```toml +[llm_configurations.openai-responses] +provider = "openai" +model_name = "gpt-5" +protocol = "responses" +``` + +如果 `provider = "openai"` 使用的是代理、中转或自建 Base URL,即使它实现了 Responses,也必须先完成该渠道的实际验证,再在条目的 `options` 中登记: + +```toml +[llm_configurations.openai-proxy-responses] +provider = "openai" +model_name = "your-model" +protocol = "responses" +options = { server_verified_protocols = ["responses"] } +``` + +官方 `https://api.openai.com/v1` 会按 OpenAI SDK 契约使用 Responses;自定义 Base URL 不会因 provider 名称为 `openai` 而自动视为官方服务。未完成服务端验证时,系统会在请求前显式报错,避免把本地 SDK 资源误发到不支持的渠道。 + +Ark Responses 也遵循同一门禁。使用 Volcengine 的 Responses 前,必须确认目标 Ark Base URL 提供 `/responses`,并在配置中登记 `options = { server_verified_protocols = ["responses"] }`;未登记时,工厂、Provider 和资源 Facade 都会在请求前显式拒绝。 + +Responses 请求会使用 `input`、`instructions` 和 `tools` 字段,并把文本增量统一转换为现有聊天流。工具定义会从 Chat Completions 的 `function` 结构转换为 Responses Function Tool;多轮历史中的 `assistant.tool_calls` 与 `role=tool` 会分别转换为 `function_call` 和 `function_call_output`,消息中的 `text`、`image_url` 和 `file` 内容块也会转换为 Responses 的 `input_text`、`input_image` 和 `input_file`。Ark Responses 另外支持 `input_audio`、`input_video` 与 `image_pixel_limit`;OpenAI Responses 对这些 Ark 专属块会在请求前显式拒绝。缺少关联 ID、无效 JSON 或不受当前 SDK 支持的内容块也会在请求前显式报错。失败、取消或不完整响应也会显式报错。Embedding 仍使用 `embeddings.create`,不会因为 LLM 协议设置而改变。 + +统一的高级调用入口是 Provider 的 `complete()`/`acomplete()`,支持 `messages`(多轮和多模态内容)、`tools`、`tool_choice`、`response_format`、`max_tokens`、`top_p`、`stop`、`seed` 和 `extra_body`。结果包含 `text`、`tool_calls`、`usage`、`finish_reason` 和原始 SDK 响应。旧版 `invoke()`/`ainvoke()` 仍只输出文本流,以保持聊天界面兼容。需要 reasoning、thinking、拒答或流式 usage 时使用 `stream_events()`/`astream_events()`。 + +非敏感供应商参数放在模型条目的 `options` 表中,工厂会拒绝 `api_key`、`access_key`、`secret_key`、`token`、`headers` 和 `base_url` 等字段: + +```toml +[llm_configurations.claude-thinking] +provider = "anthropic" +model_name = "claude-sonnet-4-6" +options = { max_tokens = 8192, thinking = { type = "adaptive" } } +``` + +Claude Sonnet 4.6 及更新的支持自适应 thinking;Sonnet 4.5 等仅支持手动模式时,应改用 `thinking = { type = "enabled", budget_tokens = 4096 }`。 + +Claude 4.5 及更新模型默认按官方契约移除 `temperature`、`top_p` 和 `top_k`。如果目标是明确支持这些字段的 Anthropic-compatible 代理,可在该模型的 `options` 中显式设置 `allow_deprecated_sampling = true`;官方端点不要开启此选项。 + +SDK 专属资源使用显式方法,不混入普通聊天请求:官方 OpenAI Provider 提供 `files`、`batches`、`vector_stores`、`audio`、`images`、`videos`、`uploads`、`conversations`、`containers`、`fine_tuning`、`evals`、`skills`、`realtime`、`webhooks`、`admin` 和 `content_provenance_checks` 等资源;这些资源也可通过 `provider.resources.native` / `provider.async_resources.async_native` 访问当前 SDK 的完整原生树。Google 提供 `files`、`caches`、`batches`、`file_search`、图像/视频、模型、调优、`auth_tokens` 和 NextGen 资源;Anthropic 提供 token count、Message Batches、Beta Files、Models,以及 `resources.beta_agents`、`resources.beta_sessions` 等 Beta 资源树;Ark 提供 Responses、Files、Batch、Tokenization、Classification、多模态 Embedding、Context、Content Generation 和 Images。兼容渠道只保证通用 Chat/Responses/Embedding 适配,默认只通过 `.native` 暴露底层客户端,不会把官方 OpenAI 的资源接口伪装成所有服务端都支持。这些方法只证明本地 SDK 调用链已适配,未替代真实账号、模型和渠道验收。 + +Embedding 文档和查询会区分任务类型:Google 使用 `RETRIEVAL_DOCUMENT` 与 `RETRIEVAL_QUERY`,OpenAI 兼容/Ark 可在 `options` 中设置 `dimensions`、`encoding_format` 等参数。批处理、文件、缓存和图像等资源必须显式调用,不会被普通 RAG 流程隐式触发。 + +这里的支持表示本地适配器能够发出 Responses 请求,不代表每个 Qwen、SiliconFlow、Ollama、LM Studio、DeepSeek 或 Grok 的兼容服务端都已实现 `/responses`。工厂的 `protocol_status()` 会分别返回 `adapter_supported` 与 `server_verified`;当前仓库未对这些外部 Base URL 做真实渠道验收,`server_verified` 默认是 `false`。启用前请确认目标 Base URL 的服务端文档和实际能力;不支持时应删除 `protocol` 或改回 `chat_completions`。 + +### 3. 定义模型实例 所有可供程序使用的模型都在 `config.toml` 中以 TOML 表定义,分为三类: @@ -56,6 +130,7 @@ model_name = "gemini-1.5-pro-latest" * **`your-custom-name`**: 您为这个模型配置起的名字。这个名字会显示在 `/config` 菜单中供您选择。例如,`google-pro`。 * **`provider`**: 指定使用哪个模型提供商的实现。这个值必须与 `src/providers/` 目录下的某个文件名(或工厂类中的标识符)相对应。例如,`google`。 * **`model_name`**: 要调用的实际模型名称/ID。这个值会直接传递给对应服务商的 API。例如,`gemini-1.5-pro-latest`。 +* **`protocol`**(仅 LLM 可选): 指定渠道协议。OpenAI 兼容渠道和 Volcengine 可使用 `chat_completions`/`responses`(Volcengine 省略时为 Ark Chat);Google、Anthropic 使用各自固定协议。Embedding 与 Rerank 配置不应填写此字段。 ## 当前默认推荐组合 @@ -75,16 +150,16 @@ model_name = "local-hash-256" ```dotenv OPENAI_API_KEY="sk-..." -OPENAI_API_BASE="https://apis.iflow.cn/v1" -DEFAULT_LLM_PROVIDER="iflow-qwen3-max" +OPENAI_API_BASE="https://api.openai.com/v1" +DEFAULT_LLM_PROVIDER="openai" ``` 并在 `config.toml` 中保留对应模型定义: ```toml -[llm_configurations.iflow-qwen3-max] +[llm_configurations.openai] provider = "openai" -model_name = "qwen3-max" +model_name = "gpt-4o" ``` ## 如何添加新模型 diff --git a/docs/user_guide/user-guide.md b/docs/user_guide/user-guide.md index 8d5a4d5..c08294f 100644 --- a/docs/user_guide/user-guide.md +++ b/docs/user_guide/user-guide.md @@ -59,7 +59,7 @@ uv run python -m scripts.embed_knowledge_base --mode standard * **Rerank重排**: 启用或禁用 Rerank 模型进行二次精排。 * **混合搜索权重**: 当使用“混合检索”时,调整向量和关键词搜索结果的权重。 * **切换模型**: - * **语言模型 (LLM)**: 动态切换当前对话使用的聊天模型(例如,从 `google` 切换到 `openai-gpt4o`)。切换后,程序会自动重载模型。 + * **语言模型 (LLM)**: 动态切换当前对话使用的聊天模型(例如,从 `google` 切换到 `openai`)。切换后,程序会自动重载模型。 * **重排模型 (Rerank)**: 切换用于精排的 Rerank 模型。 * **查看当前完整配置**: 显示所有当前生效的聊天和模型参数。 From b9e371096de35316c08ffaf30456bb59b0caee10 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 10:39:25 +0800 Subject: [PATCH 09/35] test: add provider, service and retrieval regressions Co-Authored-By: Claude Opus 5 (1M context) --- tests/chat/test_core.py | 146 +- tests/etl/test_pipeline.py | 9 +- tests/providers/test_factory.py | 119 +- tests/providers/test_failure_contracts.py | 160 +- tests/providers/test_google_genai.py | 425 +- tests/providers/test_protocol_adapters.py | 3087 +++++++++ tests/providers/test_rerank_contracts.py | 104 + tests/providers/test_sdk_capabilities.py | 5547 +++++++++++++++++ .../retrieval/test_parent_child_retrieval.py | 214 +- tests/retrieval/vdb/test_factory.py | 27 +- .../vdb/test_faiss_sidecar_roundtrip.py | 20 + .../retrieval/vdb/test_snapshot_repository.py | 16 + tests/services/test_chat_service.py | 127 + tests/services/test_embedding_service.py | 181 + .../services/test_knowledge_build_service.py | 72 + tests/test_config.py | 127 +- tests/test_main_entry.py | 1 - tests/test_release_scripts.py | 37 +- 18 files changed, 10363 insertions(+), 56 deletions(-) create mode 100644 tests/providers/test_protocol_adapters.py create mode 100644 tests/providers/test_rerank_contracts.py create mode 100644 tests/providers/test_sdk_capabilities.py create mode 100644 tests/services/test_chat_service.py create mode 100644 tests/services/test_embedding_service.py create mode 100644 tests/services/test_knowledge_build_service.py diff --git a/tests/chat/test_core.py b/tests/chat/test_core.py index bf8dbaf..5561068 100644 --- a/tests/chat/test_core.py +++ b/tests/chat/test_core.py @@ -3,9 +3,60 @@ from rich.console import Console +from src.chat.core import Chatbot, _safe_exception_text, start_chat_session_async from src.runtime.contracts import SessionConfig from src.utils.config import ModelDetail, RetrievalMethod -from src.chat.core import Chatbot, start_chat_session_async +from src.utils.log_manager import RedactingFormatter + + +def test_safe_exception_text_redacts_credentials(): + text = _safe_exception_text(RuntimeError("Authorization: Bearer sk-secret-token")) + assert "sk-secret-token" not in text + assert "REDACTED" in text + + +def test_safe_exception_text_redacts_json_credentials(): + text = _safe_exception_text(RuntimeError('{"api_key": "secret-value"}')) + assert "secret-value" not in text + assert "REDACTED" in text + + +def test_log_formatter_redacts_credentials_from_traceback(): + import logging + import sys + + try: + raise RuntimeError("Authorization: Bearer sk-secret-token") + except RuntimeError: + record = logging.LogRecord( + "chat_logger", + logging.ERROR, + __file__, + 1, + "request failed", + (), + sys.exc_info(), + ) + + rendered = RedactingFormatter("%(message)s").format(record) + assert "sk-secret-token" not in rendered + assert "[REDACTED]" in rendered + + +def test_chatbot_close_disposes_llm_and_retrieval_service(): + llm = SimpleNamespace(close=lambda: setattr(llm, "closed", True), closed=False) + retrieval = SimpleNamespace(close=lambda: setattr(retrieval, "closed", True), closed=False) + bot = object.__new__(Chatbot) + bot.llm_model = llm + bot.chat_service = object() + bot.retrieval_service = retrieval + + bot.close() + + assert llm.closed is True + assert retrieval.closed is True + assert bot.llm_model is None + assert bot.retrieval_service is None class FakePromptSession: @@ -20,7 +71,7 @@ class FakeChatbot: def __init__(self, _console: Console): self.llm_model = object() self.chat_config = { - "active_llm_configuration": "iflow-qwen3-max", + "active_llm_configuration": "openai", "active_rerank_configuration": "siliconflow", "retrieval_method": SimpleNamespace(value="混合检索"), "vector_weight": 0.3, @@ -31,7 +82,7 @@ def __init__(self, _console: Console): "top_k": 5, "score_threshold": 0.4, "llm_configurations": { - "iflow-qwen3-max": SimpleNamespace(provider="openai", model_name="qwen3-max"), + "openai": SimpleNamespace(provider="openai", model_name="gpt-4o"), }, "rerank_configurations": { "siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank"), @@ -109,6 +160,7 @@ async def chat_async(self, user_input): def test_chat_async_retrieves_with_intent(): retrieval_queries = [] + model_calls = [] class DummyLogger: def info(self, *_args, **_kwargs): @@ -118,7 +170,8 @@ def error(self, *_args, **_kwargs): return None class DummyLLM: - async def ainvoke(self, *_args, **_kwargs): + async def ainvoke(self, *_args, **kwargs): + model_calls.append(kwargs) yield "回答" async def fake_identify(self, _user_query): @@ -148,6 +201,7 @@ async def consume(): assert retrieval_queries == ["清除浏览器缓存的操作步骤"] assert chunks == ["回答"] + assert "temperature" not in model_calls[0] def test_reload_llm_keeps_existing_model_on_failure(monkeypatch): @@ -165,7 +219,7 @@ async def ainvoke(self, *_args, **_kwargs): monkeypatch.setattr( "src.chat.core.ModelProviderFactory.get_llm_provider", - lambda _llm_key: (_ for _ in ()).throw(RuntimeError("reload failed")), + lambda _llm_key, _configurations=None: (_ for _ in ()).throw(RuntimeError("reload failed")), ) assert bot.reload_llm() is False @@ -173,6 +227,50 @@ async def ainvoke(self, *_args, **_kwargs): assert bot.chat_service is old_chat_service +def test_apply_config_update_async_keeps_runtime_on_chat_service_failure(monkeypatch): + class ClosableModel: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + old_model = ClosableModel() + new_model = ClosableModel() + old_chat_service = object() + messages = [] + + class DummySessionConfig: + active_llm_configuration = "new-model" + + def __setitem__(self, key, value): + setattr(self, key, value) + + bot = object.__new__(Chatbot) + bot.console = SimpleNamespace(print=lambda message: messages.append(str(message))) + bot.logger = SimpleNamespace(warning=lambda *_args, **_kwargs: None) + bot.session_config = DummySessionConfig() + bot.retrieval_service = object() + bot.llm_model = old_model + bot.chat_service = old_chat_service + + monkeypatch.setattr( + "src.chat.core.ModelProviderFactory.get_llm_provider", + lambda *_args, **_kwargs: new_model, + ) + monkeypatch.setattr( + "src.chat.core.ChatService", + lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("service init failed")), + ) + + asyncio.run(bot.apply_config_update_async({"active_llm_configuration": "new-model"}, True)) + + assert bot.llm_model is old_model + assert bot.chat_service is old_chat_service + assert new_model.closed is True + assert any("LLM 切换失败" in message for message in messages) + + def test_apply_config_update_reverts_llm_key_when_reload_fails(monkeypatch): class DummySessionConfig: def __init__(self, active_llm_configuration: str): @@ -195,6 +293,42 @@ def __setitem__(self, key, value): assert any("LLM 切换失败" in message for message in messages) +def test_apply_config_update_restores_all_llm_configurations_on_reload_failure(monkeypatch): + old_detail = ModelDetail(provider="openai", model_name="old-model") + new_detail = ModelDetail(provider="qwen", model_name="new-model") + bot = object.__new__(Chatbot) + bot.console = SimpleNamespace(print=lambda _message: None) + bot.session_config = SessionConfig( + retrieval_method=RetrievalMethod.HYBRID_SEARCH, + vector_weight=0.3, + keyword_weight=0.7, + hybrid_fusion_strategy="rrf", + retrieval_candidate_multiplier=3, + rerank_enabled=False, + top_k=5, + score_threshold=0.4, + active_llm_configuration="old-model", + active_rerank_configuration="siliconflow", + llm_configurations={"old-model": old_detail}, + rerank_configurations={}, + chat_temperature=0.7, + ) + bot.llm_model = object() + bot.chat_service = object() + monkeypatch.setattr(bot, "reload_llm", lambda: False) + + bot.apply_config_update( + { + "active_llm_configuration": "new-model", + "llm_configurations": {"new-model": new_detail}, + }, + llm_needs_reload=True, + ) + + assert bot.session_config.active_llm_configuration == "old-model" + assert bot.session_config.llm_configurations == {"old-model": old_detail} + + def test_start_chat_session_async_reverts_llm_after_editor_mutation(monkeypatch): class RealishChatbot(FakeChatbot): last_instance = None @@ -214,7 +348,7 @@ def __init__(self, _console: Console): score_threshold=0.4, active_llm_configuration="old-model", active_rerank_configuration="siliconflow", - llm_configurations={"old-model": ModelDetail(provider="openai", model_name="qwen3-max")}, + llm_configurations={"old-model": ModelDetail(provider="openai", model_name="gpt-4o")}, rerank_configurations={"siliconflow": ModelDetail(provider="siliconflow", model_name="rerank")}, chat_temperature=0.7, ) diff --git a/tests/etl/test_pipeline.py b/tests/etl/test_pipeline.py index 7397311..398adc8 100644 --- a/tests/etl/test_pipeline.py +++ b/tests/etl/test_pipeline.py @@ -1,13 +1,14 @@ -import pytest from pathlib import Path from unittest.mock import MagicMock -from src.etl.pipeline import Pipeline -from src.etl.extractors.markdown_extractor import MarkdownExtractor +import pytest + from src.etl.cleaners.basic_cleaner import BasicCleaner +from src.etl.extractors.markdown_extractor import MarkdownExtractor +from src.etl.pipeline import Pipeline from src.etl.splitters.recursive_text_splitter import RecursiveTextSplitter from src.models.document import Document -from src.utils.config import get_settings # 导入 get_settings 函数 +from src.utils.config import get_settings # 导入 get_settings 函数 # 用于测试的 Markdown 示例内容 SAMPLE_MARKDOWN = """ diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index 6935228..3daba5e 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -1,27 +1,34 @@ -import pytest +import sys # 导入 sys 模块 +from collections.abc import AsyncGenerator, Generator +from typing import Any from unittest.mock import MagicMock, patch -from typing import Any, Generator, List, AsyncGenerator -import sys # 导入 sys 模块 -from src.utils.config import Settings, get_settings -from src.providers.__base__.model_provider import LargeLanguageModel, TextEmbeddingModel, RerankModel -from src.models.document import Document -from src.utils.config import ModelDetail -from src.providers.factory import ModelProviderFactory # 在这里导入 ModelProviderFactory +import pytest + +from src.providers.__base__.model_provider import ( + LargeLanguageModel, + RerankModel, + TextEmbeddingModel, +) +from src.providers.factory import ( + ModelProviderFactory, # 在这里导入 ModelProviderFactory +) +from src.utils.config import ModelDetail, Settings + # Mock 类定义 class MockLLMProvider(LargeLanguageModel): def __init__(self, model_name: str): self.model_name = model_name - def invoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: List[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> Generator[str, None, None]: + def invoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> Generator[str, None, None]: """模拟 LLM 聊天响应""" if stream: yield f"Mock LLM stream response for {self.model_name}" else: yield f"Mock LLM response for {self.model_name}" - async def ainvoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: List[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> AsyncGenerator[str, None]: + async def ainvoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> AsyncGenerator[str, None]: """模拟异步 LLM 聊天响应""" if stream: yield f"Mock Async LLM stream response for {self.model_name}" @@ -67,13 +74,15 @@ def mock_settings(): mock_embedding_config = ModelDetail(provider="mock_embedding", model_name="mock-embedding-model") mock_rerank_config = ModelDetail(provider="mock_rerank", model_name="mock-rerank-model") mock_siliconflow_rerank_config = ModelDetail(provider="siliconflow", model_name="mock-siliconflow-rerank-model") + mock_qwen_rerank_config = ModelDetail(provider="qwen", model_name="qwen-rerank-model") mock_settings_instance = MagicMock(spec=Settings) mock_settings_instance.llm_configurations = {"default_llm": mock_llm_config} mock_settings_instance.embedding_configurations = {"default_embedding": mock_embedding_config} mock_settings_instance.rerank_configurations = { "default_rerank": mock_rerank_config, - "siliconflow_rerank_key": mock_siliconflow_rerank_config + "siliconflow_rerank_key": mock_siliconflow_rerank_config, + "qwen_rerank_key": mock_qwen_rerank_config, } mock_settings_instance.siliconflow_api_key = "mock_siliconflow_api_key" mock_settings_instance.siliconflow_base_url = "http://mock-siliconflow-base-url.com" @@ -107,7 +116,9 @@ def patch_settings(mock_settings, monkeypatch): del sys.modules[module_name] # 重新导入 ModelProviderFactory,确保它加载的是最新的版本 - from src.providers.factory import ModelProviderFactory as ReloadedModelProviderFactory + from src.providers.factory import ( + ModelProviderFactory as ReloadedModelProviderFactory, + ) # 直接模拟 _get_provider_class 方法 def mock_get_provider_class(provider_name: str): @@ -115,11 +126,9 @@ def mock_get_provider_class(provider_name: str): return MockLLMProvider elif provider_name == "mock_embedding": return MockEmbeddingProvider - elif provider_name == "mock_rerank": + elif provider_name == "mock_rerank" or provider_name == "siliconflow_rerank": return MockRerankProvider - elif provider_name == "siliconflow_rerank": - return MockRerankProvider - elif provider_name == "google": + elif provider_name == "google" or provider_name == "qwen": return MockLLMProvider else: raise ValueError(f"不支持的模型提供商: {provider_name}") @@ -143,6 +152,50 @@ def test_get_llm_provider_not_found(): with pytest.raises(ValueError, match="在LLM配置中未找到key: non_existent_key"): ModelProviderFactory.get_llm_provider("non_existent_key") + +def test_factory_uses_explicit_configuration_mapping(): + configurations = {"custom": ModelDetail(provider="mock_llm", model_name="custom-model")} + + provider = ModelProviderFactory.get_llm_provider("custom", configurations) + + assert provider.model_name == "custom-model" + + +def test_factory_does_not_pass_fixed_protocol_to_provider_constructor(): + configurations = { + "google-model": ModelDetail( + provider="google", model_name="gemini-model", protocol="generate_content" + ) + } + + provider = ModelProviderFactory.get_llm_provider("google-model", configurations) + + assert isinstance(provider, MockLLMProvider) + assert provider.model_name == "gemini-model" + + +def test_factory_normalizes_direct_string_protocol_values(): + provider = ModelProviderFactory._create_provider( + "mock_llm", + "mock-model", + "llm", + LargeLanguageModel, + protocol="chat", + ) + + assert isinstance(provider, MockLLMProvider) + + +def test_factory_rejects_options_for_provider_without_options_parameter(): + with pytest.raises(ValueError, match="不支持 options"): + ModelProviderFactory._create_provider( + "mock_embedding", + "mock-model", + "embedding", + TextEmbeddingModel, + options={"dimensions": 2}, + ) + def test_get_embedding_provider_success(): """测试成功获取 Embedding 提供商""" embedding_provider = ModelProviderFactory.get_embedding_provider("default_embedding") @@ -154,6 +207,35 @@ def test_get_embedding_provider_not_found(): with pytest.raises(ValueError, match="在Embedding配置中未找到key: non_existent_key"): ModelProviderFactory.get_embedding_provider("non_existent_key") + +def test_get_embedding_provider_rejects_llm_only_provider(): + configurations = {"chat-only": ModelDetail(provider="deepseek", model_name="deepseek-chat")} + + with pytest.raises(TypeError, match="提供商 deepseek 不支持 embedding 能力"): + ModelProviderFactory.get_embedding_provider("chat-only", configurations) + + +def test_get_embedding_provider_rejects_protocol_configuration(): + configurations = { + "embedding": ModelDetail( + provider="mock_embedding", model_name="embedding-model", protocol="responses" + ) + } + + with pytest.raises(ValueError, match="Embedding 配置不支持 protocol"): + ModelProviderFactory.get_embedding_provider("embedding", configurations) + + +def test_get_rerank_provider_rejects_protocol_configuration(): + configurations = { + "rerank": ModelDetail( + provider="mock_rerank", model_name="rerank-model", protocol="responses" + ) + } + + with pytest.raises(ValueError, match="Rerank 配置不支持 protocol"): + ModelProviderFactory.get_rerank_provider("rerank", configurations) + def test_get_rerank_provider_success(): """测试成功获取 Rerank 提供商""" rerank_provider = ModelProviderFactory.get_rerank_provider("default_rerank") @@ -171,6 +253,11 @@ def test_get_rerank_provider_not_found(): with pytest.raises(ValueError, match="在Rerank配置中未找到key: non_existent_key"): ModelProviderFactory.get_rerank_provider("non_existent_key") + +def test_get_rerank_provider_rejects_provider_without_rerank_capability(): + with pytest.raises(TypeError, match="提供商 qwen 不支持 rerank 能力"): + ModelProviderFactory.get_rerank_provider("qwen_rerank_key") + def test_unsupported_provider_type(): """测试 _get_provider_class 方法处理不支持的提供商名称""" with pytest.raises(ValueError, match="不支持的模型提供商: unsupported_provider"): diff --git a/tests/providers/test_failure_contracts.py b/tests/providers/test_failure_contracts.py index 378e019..a814ec5 100644 --- a/tests/providers/test_failure_contracts.py +++ b/tests/providers/test_failure_contracts.py @@ -1,15 +1,23 @@ -# -*- coding: utf-8 -*- import asyncio import importlib from types import SimpleNamespace +from unittest.mock import MagicMock import pytest import src.providers.factory as factory_module +from src.providers.__base__.model_provider import ( + CompletionRequest, + LargeLanguageModel, + iterate_async, +) +from src.providers.anthropic import AnthropicProvider from src.providers.factory import ModelProviderFactory +from src.providers.google import GoogleProvider from src.providers.grok import GrokProvider from src.providers.openai import OpenAIProvider from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.volcengine import VolcengineProvider from src.utils.config import ModelDetail @@ -26,6 +34,77 @@ async def create(*_args, **_kwargs): raise RuntimeError("boom-embed") +class RequestCaptureModel(LargeLanguageModel): + def invoke(self, **_kwargs): + yield "ok" + + async def ainvoke(self, **_kwargs): + yield "ok" + + +def test_iterate_async_awaits_async_close_on_sync_iterable(): + class AsyncClosableIterable: + def __init__(self): + self.closed = False + + def __iter__(self): + return iter(["ok"]) + + async def close(self): + self.closed = True + + value = AsyncClosableIterable() + + async def consume() -> list[str]: + return [item async for item in iterate_async(value)] + + assert asyncio.run(consume()) == ["ok"] + assert value.closed is True + + +def test_base_request_and_kwargs_conflict_is_explicit(): + provider = RequestCaptureModel() + request = CompletionRequest(prompt="from-request") + + with pytest.raises(ValueError, match="不能同时传 request"): + provider.complete(request, prompt="from-kwargs") + with pytest.raises(ValueError, match="不能同时传 request"): + asyncio.run(provider.acomplete(request, prompt="from-kwargs")) + with pytest.raises(ValueError, match="不能同时传 request"): + list(provider.stream_events(request, prompt="from-kwargs")) + + async def consume() -> None: + async for _event in provider.astream_events(request, prompt="from-kwargs"): + pass + + with pytest.raises(ValueError, match="不能同时传 request"): + asyncio.run(consume()) + + +@pytest.mark.parametrize( + ("provider_class", "method_name"), + [ + (OpenAICompatibleProvider, "complete"), + (OpenAICompatibleProvider, "stream_events"), + (AnthropicProvider, "complete"), + (AnthropicProvider, "stream_events"), + (GoogleProvider, "complete"), + (GoogleProvider, "count_tokens"), + (VolcengineProvider, "complete"), + (VolcengineProvider, "stream_events"), + ], +) +def test_provider_request_and_kwargs_conflict_is_explicit(provider_class, method_name): + provider = object.__new__(provider_class) + request = CompletionRequest(prompt="from-request") + method = getattr(provider, method_name) + + with pytest.raises(ValueError, match="不能同时传 request"): + result = method(request, prompt="from-kwargs") + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + list(result) + + def test_openai_provider_async_failure_raises(monkeypatch): fake_settings = SimpleNamespace(openai_api_key="token", openai_api_base="http://example.com") monkeypatch.setattr("src.providers.openai.get_settings", lambda: fake_settings) @@ -43,7 +122,6 @@ async def consume() -> None: with pytest.raises(RuntimeError, match="boom-embed"): asyncio.run(provider.aembed_documents(["doc"])) - def test_openai_compatible_provider_async_failure_raises(monkeypatch): fake_settings = SimpleNamespace(deepseek_api_key="token", deepseek_base_url="http://example.com") monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) @@ -61,6 +139,60 @@ async def consume() -> None: with pytest.raises(RuntimeError, match="boom-embed"): asyncio.run(provider.aembed_documents(["doc"])) +def test_openai_provider_reuses_compatible_adapter(monkeypatch): + fake_settings = SimpleNamespace(openai_api_key="token", openai_api_base="http://example.com") + monkeypatch.setattr("src.providers.openai.get_settings", lambda: fake_settings) + + provider = OpenAIProvider("demo") + + assert isinstance(provider, OpenAICompatibleProvider) + assert provider._base_url == "http://example.com" + + +def test_openai_provider_preserves_missing_key_error(monkeypatch): + monkeypatch.setattr( + "src.providers.openai.get_settings", + lambda: SimpleNamespace(openai_api_key=None, openai_api_base="http://example.com"), + ) + + with pytest.raises(ValueError, match="OpenAI配置不完整:缺少 OPENAI_API_KEY"): + OpenAIProvider("demo") + + +def test_openai_compatible_provider_normalizes_hyphenated_settings(monkeypatch): + fake_settings = SimpleNamespace( + lm_studio_api_key=None, + lm_studio_base_url="http://localhost:1234/v1", + ) + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) + + provider = OpenAICompatibleProvider("demo", "lm-studio") + + assert provider._api_key == "no-key-required" + assert provider._base_url == "http://localhost:1234/v1" + + +def test_openai_compatible_provider_forwards_tools(monkeypatch): + fake_settings = SimpleNamespace(deepseek_api_key="token", deepseek_base_url="http://example.com") + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) + provider = OpenAICompatibleProvider("demo", "deepseek") + + client = MagicMock() + client.chat.completions.create.return_value = SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))] + ) + provider._client = client + tools = [{"type": "function", "function": {"name": "lookup"}}] + + assert list(provider.invoke("hello", tools=tools, stream=False)) == ["ok"] + + request = client.chat.completions.create.call_args.kwargs + assert request["tools"] == tools + assert request["messages"] == [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ] + def test_grok_provider_can_be_loaded_by_factory(monkeypatch): fake_settings = SimpleNamespace( @@ -77,6 +209,30 @@ def test_grok_provider_can_be_loaded_by_factory(monkeypatch): assert provider.__class__.__module__ == "src.providers.grok" +def test_factory_caches_provider_class(monkeypatch): + factory = factory_module.ModelProviderFactory + factory._get_provider_class.cache_clear() + monkeypatch.setattr( + factory, + "_provider_map", + {"cached": {"module": "fake.provider", "class": "CachedProvider"}}, + ) + module = SimpleNamespace(CachedProvider=object) + imported_modules = [] + + def import_module(module_name): + imported_modules.append(module_name) + return module + + monkeypatch.setattr(factory_module.importlib, "import_module", import_module) + + first = factory._get_provider_class("cached") + second = factory._get_provider_class("cached") + + assert first is second + assert imported_modules == ["fake.provider"] + + @pytest.mark.parametrize( ("provider_name", "module_name", "class_name"), [ diff --git a/tests/providers/test_google_genai.py b/tests/providers/test_google_genai.py index d9c58a3..bf27544 100644 --- a/tests/providers/test_google_genai.py +++ b/tests/providers/test_google_genai.py @@ -1,8 +1,13 @@ -import pytest +import asyncio +from types import SimpleNamespace from unittest.mock import MagicMock, patch + +import pytest + import src.providers.google as google_module from src.providers.google import GoogleProvider + @pytest.fixture(autouse=True) def mock_settings(monkeypatch): """强制模拟 settings 并在环境中设置 key""" @@ -32,6 +37,399 @@ def test_google_provider_get_client(mock_settings, mock_genai_client): from src.providers.google import genai genai.Client.assert_called_once_with(api_key="fake_key") + +def test_google_provider_uses_credentials_without_api_key(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = "should-not-be-forwarded" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"credentials": credentials, "vertexai": True}, + ) + + provider._get_client() + + client_class.assert_called_once_with(credentials=credentials, vertexai=True) + + +def test_google_provider_accepts_gemini_api_key_alias(monkeypatch): + settings = SimpleNamespace(google_api_key=None, gemini_api_key="gemini-key") + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with(api_key="gemini-key") + + +def test_google_api_key_takes_precedence_over_gemini_alias(monkeypatch): + settings = SimpleNamespace( + google_api_key="google-key", gemini_api_key="gemini-key" + ) + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with(api_key="google-key") + + +def test_google_provider_accepts_credentials_without_google_api_key(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", options={"credentials": credentials} + ) + + provider._get_client() + + client_class.assert_called_once_with(credentials=credentials, vertexai=True) + + +def test_google_credentials_reject_explicit_developer_api_mode(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"credentials": credentials, "vertexai": False}, + ) + with patch.object(google_module.genai, "Client"), pytest.raises( + ValueError, match="Vertex/Enterprise" + ): + provider._get_client() + + +def test_google_vertex_uses_adc_without_api_key(monkeypatch): + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", options={"vertexai": True} + ) + + provider._get_client() + + client_class.assert_called_once_with(vertexai=True) + + +def test_google_vertex_dotenv_settings_are_forwarded_to_sdk(monkeypatch): + settings = SimpleNamespace( + google_api_key=None, + google_genai_use_vertexai=True, + google_genai_use_enterprise=None, + google_cloud_project="dotenv-project", + google_cloud_location="asia-east1", + google_application_credentials=None, + ) + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with( + vertexai=True, + project="dotenv-project", + location="asia-east1", + ) + + +def test_google_get_client_requires_explicit_auth_without_key_or_vertex_mode(monkeypatch): + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + provider = GoogleProvider(model_name="gemini-1.5-flash", options={}) + + with patch.object(google_module.genai, "Client") as client_class, pytest.raises( + ValueError, match="GOOGLE_API_KEY|Vertex/Enterprise" + ): + provider._get_client() + + client_class.assert_not_called() + + +@pytest.mark.parametrize( + "http_options", + [ + {"headers": {"Authorization": "Bearer secret"}}, + {"headers": {"X-API-Key": "secret"}}, + {"client_args": {"headers": {"X-Auth-Token": "secret"}}}, + {"client_args": {"params": {"access_token": "secret"}}}, + {"async_client_args": {"timeout": 1000}}, + {"async_client_args": {"params": {"trace_id": "trace"}}}, + {"base_url": "https://proxy.invalid/v1"}, + {"base_url": "https://proxy.invalid/v1?api_key=secret"}, + {"base_url": "https://user:password@proxy.invalid/v1"}, + {"httpx_client": object()}, + ], +) +def test_google_http_options_reject_credentials_or_custom_clients(http_options): + with pytest.raises(ValueError, match="凭证|自定义客户端|连接覆盖"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": http_options}, + ) + + +def test_google_http_options_allow_safe_headers_and_sdk_object(): + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + provider._validate_options(provider._options) + sdk_options = google_module.types.HttpOptions(headers={"X-Trace-ID": "trace"}) + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": sdk_options}, + ) + provider._validate_options(provider._options) + + +def test_google_http_options_scan_header_values_and_avoid_substring_false_positives(): + with pytest.raises(ValueError, match="凭证"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {"headers": {"X-Trace": "Bearer secret-token"}}}, + ) + + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={ + "http_options": { + "headers": { + "X-Authorization-Count": "1", + "X-Credential-Provider": "internal", + } + } + }, + ) + provider._validate_options(provider._options) + + +def test_google_http_options_reject_connection_override_containers(): + for key in ("client_args", "async_client_args"): + with pytest.raises(ValueError, match="连接覆盖"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {key: {"timeout": 1000}}}, + ) + + +def test_google_invoke_non_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + client = MagicMock() + client.models.generate_content.return_value = response + + with patch.object(provider, "_get_client", return_value=client), pytest.raises( + RuntimeError, match="安全策略|拒答" + ): + list(provider.invoke("hello", stream=False)) + + +def test_google_invoke_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + chunk = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + client = MagicMock() + client.models.generate_content_stream.return_value = iter([chunk]) + + with patch.object(provider, "_get_client", return_value=client), pytest.raises( + RuntimeError, match="安全策略|拒答" + ): + list(provider.invoke("hello", stream=True)) + + +def test_google_stream_events_emits_finish_for_top_level_finish_reason(): + events = GoogleProvider._stream_events_from_chunk({"finish_reason": "STOP"}) + + finish_events = [event for event in events if event.type == "finish"] + assert len(finish_events) == 1 + assert finish_events[0].finish_reason == "STOP" + + +def test_google_finish_reason_enum_is_normalized_to_wire_value(): + from google.genai import types + + candidate = SimpleNamespace( + finish_reason=types.FinishReason.STOP, + content=SimpleNamespace(parts=[]), + ) + response = SimpleNamespace(candidates=[candidate]) + + result = GoogleProvider._extract_result(response) + events = GoogleProvider._stream_events_from_chunk(response) + + assert result.finish_reason == "STOP" + assert [event.finish_reason for event in events if event.type == "finish"] == ["STOP"] + + +def test_google_ainvoke_non_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + + class Models: + async def generate_content(self, **_kwargs): + return response + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + + async def consume(): + with patch.object(provider, "_get_client", return_value=client): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="安全策略|拒答"): + asyncio.run(consume()) + + +def test_google_ainvoke_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + chunk = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + + class AsyncChunks: + def __aiter__(self): + async def chunks(): + yield chunk + + return chunks() + + class Models: + def generate_content_stream(self, **_kwargs): + return AsyncChunks() + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + + async def consume(): + with patch.object(provider, "_get_client", return_value=client): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + with pytest.raises(RuntimeError, match="安全策略|拒答"): + asyncio.run(consume()) + + +def test_google_debug_config_mapping_is_converted_to_sdk_type(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"debug_config": {"client_mode": "replay", "replay_id": "r1"}}, + ) + provider._get_client() + + debug_config = client_class.call_args.kwargs["debug_config"] + assert debug_config.client_mode == "replay" + assert debug_config.replay_id == "r1" + + +def test_google_unknown_option_fails_at_provider_boundary(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + + with pytest.raises(ValueError, match="不支持的字段.*typo_option"): + GoogleProvider(model_name="gemini-1.5-flash", options={"typo_option": True}) + + +@pytest.mark.parametrize("value", ["false", "0", "off", "no", ""]) +def test_google_vertex_mode_parses_false_string_values(value): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {"vertexai": value} + + assert provider._configured_vertex_mode() is False + + +def test_google_endpoint_modes_reject_conflicting_explicit_flags(): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {"enterprise": True, "vertexai": False} + + with pytest.raises(ValueError, match="enterprise.*vertexai.*冲突"): + provider._configured_vertex_mode() + + +def test_google_endpoint_modes_reject_conflicting_environment_flags(monkeypatch): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {} + monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "true") + monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false") + + with pytest.raises(ValueError, match="GOOGLE_GENAI_USE_ENTERPRISE.*冲突"): + provider._configured_vertex_mode() + + +def test_google_get_client_reuses_falsey_client(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + + class FalseyClient: + def __bool__(self): + return False + + client = FalseyClient() + provider = GoogleProvider.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + provider._client = client + + with patch.object(google_module.genai, "Client") as client_class: + assert provider._get_client() is client + client_class.assert_not_called() + def test_google_provider_invoke_non_stream(mock_settings, mock_genai_client): provider = GoogleProvider(model_name="gemini-1.5-flash") @@ -65,3 +463,28 @@ def test_google_provider_embed_documents(mock_settings, mock_genai_client): assert result == [[0.1, 0.2], [0.3, 0.4]] mock_genai_client.models.embed_content.assert_called_once() + + +def test_google_embedding_rejects_request_credentials_before_client_init(mock_settings): + provider = GoogleProvider(model_name="embedding-001") + + with ( + patch.object(provider, "_get_client", side_effect=AssertionError("client must not initialize")), + pytest.raises(ValueError, match="凭证"), + ): + provider.embed_documents( + ["text"], + extra_headers={"Authorization": "Bearer secret-token"}, + ) + + async def consume() -> None: + with ( + patch.object(provider, "_get_client", side_effect=AssertionError("client must not initialize")), + pytest.raises(ValueError, match="凭证"), + ): + await provider.aembed_documents( + ["text"], + extra_headers={"Authorization": "Bearer secret-token"}, + ) + + asyncio.run(consume()) diff --git a/tests/providers/test_protocol_adapters.py b/tests/providers/test_protocol_adapters.py new file mode 100644 index 0000000..15600e1 --- /dev/null +++ b/tests/providers/test_protocol_adapters.py @@ -0,0 +1,3087 @@ +import asyncio +import base64 +import inspect +import struct +from types import SimpleNamespace + +import pytest + +import src.providers.__base__.model_provider as model_provider_module +import src.providers.anthropic as anthropic_provider_module +import src.providers.openai_compatible as openai_compatible_module +import src.providers.volcengine as volcengine_module +from src.providers.__base__.model_provider import ( + CompletionRequest, + normalize_responses_input, +) +from src.providers.anthropic import AnthropicProvider +from src.providers.deepseek import DeepSeekProvider +from src.providers.google import GoogleProvider +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.volcengine import VolcengineProvider + +TOOLS = [ + { + "type": "function", + "function": { + "name": "lookup_order", + "description": "查询订单", + "parameters": { + "type": "object", + "properties": {"order_id": {"type": "string"}}, + "required": ["order_id"], + }, + }, + } +] + + +def test_anthropic_converts_openai_tool_schema(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + params = AnthropicProvider._build_message_params( + provider, "hello", "system", TOOLS, 0.2 + ) + + assert params["tools"] == [ + { + "name": "lookup_order", + "description": "查询订单", + "input_schema": TOOLS[0]["function"]["parameters"], + } + ] + + +def test_anthropic_parse_rejects_user_profile_on_non_beta_sdk_path(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + with pytest.raises(ValueError, match="parse 不支持 user"): + provider._build_message_params( + prompt="hello", + user="profile-1", + for_parse=True, + ) + + +def test_anthropic_rejects_unlinked_tool_result(): + with pytest.raises(ValueError, match="tool_call_id"): + AnthropicProvider._convert_messages( + [{"role": "tool", "content": "{}"}] + ) + + +def test_anthropic_rejects_malformed_tool_call_arguments(): + with pytest.raises(ValueError, match="arguments.*有效 JSON"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{"}, + } + ], + } + ] + ) + + +def test_anthropic_rejects_image_content_without_url(): + with pytest.raises(ValueError, match="图片内容缺少有效 image_url"): + AnthropicProvider._convert_content( + [{"type": "image_url", "image_url": {}}] + ) + + +def test_anthropic_rejects_invalid_image_data_uri(): + with pytest.raises(ValueError, match="Base64"): + AnthropicProvider._convert_content( + [{"type": "image_url", "image_url": "data:image/png;base64,invalid"}] + ) + + +def test_anthropic_rejects_tool_call_without_id_or_name(): + with pytest.raises(ValueError, match="缺少 id"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [{"function": {"name": "lookup", "arguments": "{}"}}], + } + ] + ) + with pytest.raises(ValueError, match="function.name"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [{"id": "call-1", "function": {"arguments": "{}"}}], + } + ] + ) + + +@pytest.mark.parametrize( + "tool", + [ + None, + {"function": "not-an-object"}, + {"function": {"name": "lookup", "parameters": []}}, + ], +) +def test_anthropic_rejects_malformed_tool_definitions(tool): + with pytest.raises(ValueError, match="工具定义"): + AnthropicProvider._convert_tools([tool]) + + +@pytest.mark.parametrize( + "tool_choice", + ["unsupported", {"type": "function"}, {"type": "tool"}], +) +def test_anthropic_rejects_malformed_tool_choices(tool_choice): + with pytest.raises(ValueError, match="tool_choice"): + AnthropicProvider._convert_tool_choice(tool_choice) + + +def test_anthropic_stream_errors_are_explicit(): + with pytest.raises(RuntimeError, match="上游失败"): + AnthropicProvider._stream_event( + SimpleNamespace( + type="session.error", + error=SimpleNamespace(message="上游失败"), + ) + ) + + +def test_google_converts_openai_tool_schema(): + converted = GoogleProvider._convert_tools(TOOLS) + + assert converted is not None + declaration = converted[0].function_declarations[0] + assert declaration.name == "lookup_order" + assert declaration.description == "查询订单" + assert declaration.parameters_json_schema == TOOLS[0]["function"]["parameters"] + + +def test_google_converts_standard_tool_result_to_function_response(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"status":"paid"}', + }, + ], + None, + None, + ) + + assert contents[0].role == "model" + function_call = next(part.function_call for part in contents[0].parts if part.function_call) + assert function_call.name == "lookup_order" + function_response = contents[1].parts[0].function_response + assert contents[1].role == "user" + assert function_response.name == "lookup_order" + assert function_response.id == "call-1" + assert function_response.response == {"status": "paid"} + + +def test_google_merges_parallel_tool_results_into_one_user_turn(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + {"id": "call-1", "function": {"name": "lookup_order", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "lookup_customer", "arguments": "{}"}}, + ], + }, + {"role": "tool", "tool_call_id": "call-1", "content": '{"status":"paid"}'}, + {"role": "tool", "tool_call_id": "call-2", "content": '{"name":"Ada"}'}, + ], + None, + None, + ) + + assert [content.role for content in contents] == ["model", "user"] + assert len(contents[1].parts) == 2 + assert [part.function_response.name for part in contents[1].parts] == [ + "lookup_order", + "lookup_customer", + ] + + +def test_google_merges_consecutive_same_role_messages(): + contents = GoogleProvider._contents( + [ + {"role": "user", "content": "第一条"}, + {"role": "user", "content": "第二条"}, + {"role": "assistant", "content": "答复"}, + ], + None, + None, + ) + + assert [content.role for content in contents] == ["user", "model"] + assert [part.text for part in contents[0].parts] == ["第一条", "第二条"] + + +def test_google_rejects_external_media_urls_and_unknown_part_types(): + with pytest.raises(ValueError, match="仅支持 data: URI、gs:// URI 或 Google Files URI"): + GoogleProvider._contents( + [ + { + "role": "user", + "content": [{"type": "image_url", "image_url": "https://example.com/a.png"}], + } + ], + None, + None, + ) + + allowed = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": "gs://bucket/a.png"}, + { + "type": "file", + "url": "https://generativelanguage.googleapis.com/v1beta/files/file-1", + }, + ], + } + ], + None, + None, + ) + assert allowed[0].parts[0].file_data.file_uri == "gs://bucket/a.png" + assert allowed[0].parts[1].file_data.file_uri.endswith("/files/file-1") + + with pytest.raises(ValueError, match="不支持的消息内容块类型"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "unknown_part", "value": "x"}]}], + None, + None, + ) + + +def test_google_media_file_ids_are_resolved_and_mime_types_forwarded(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + { + "type": "input_image", + "image_url": {"file_id": "file-image", "mime_type": "image/png"}, + }, + { + "type": "file", + "file": {"file_id": "file-document", "mime_type": "application/pdf"}, + }, + ], + } + ], + None, + None, + ) + + image_data = contents[0].parts[0].file_data + document_data = contents[0].parts[1].file_data + assert image_data.file_uri.endswith("/v1beta/files/file-image") + assert image_data.mime_type == "image/png" + assert document_data.file_uri.endswith("/v1beta/files/file-document") + assert document_data.mime_type == "application/pdf" + + +def test_google_vertex_rejects_gemini_files_ids(): + for value in ( + "file-image", + "files/file-image", + "https://generativelanguage.googleapis.com/v1beta/files/file-image", + ): + with pytest.raises(ValueError, match="Vertex/Enterprise"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "input_image", "image_url": value}]}], + None, + None, + vertex_mode=True, + ) + + +def test_google_vertex_accepts_cloud_storage_and_public_https_media(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "input_image", "image_url": "gs://bucket/image.png"}, + {"type": "file", "url": "https://cdn.example.com/document.pdf?version=1"}, + ], + } + ], + None, + None, + vertex_mode=True, + ) + + assert contents[0].parts[0].file_data.file_uri == "gs://bucket/image.png" + assert contents[0].parts[1].file_data.file_uri == "https://cdn.example.com/document.pdf?version=1" + + +@pytest.mark.parametrize("file_id", ["", " ", "files/", "file/id"]) +def test_google_media_file_ids_reject_invalid_values(file_id): + with pytest.raises(ValueError, match="file_id|有效 URI"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "input_image", "file_id": file_id}]}], + None, + None, + ) + + +def test_google_rejects_malformed_tool_arguments_and_results(): + with pytest.raises(ValueError, match="arguments.*有效 JSON"): + GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{"}, + } + ], + } + ], + None, + None, + ) + + with pytest.raises(ValueError, match="content.*有效 JSON"): + GoogleProvider._contents( + [ + {"role": "tool", "tool_call_id": "call-1", "name": "lookup", "content": "{"} + ], + None, + None, + ) + + +def test_google_rejects_invalid_data_uri_base64(): + with pytest.raises(ValueError, match="Base64"): + GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": "data:image/png;base64,not-base64"} + ], + } + ], + None, + None, + ) + + +@pytest.mark.parametrize( + "part", + [ + {"type": "image_url", "image_url": {}}, + {"type": "audio_url"}, + {"type": "video_url", "video_url": {}}, + {"type": "file", "file": {}}, + ], +) +def test_google_rejects_media_content_without_uri(part): + with pytest.raises(ValueError, match="(图片|媒体)内容缺少有效 URI"): + GoogleProvider._contents( + [{"role": "user", "content": [part]}], + None, + None, + ) + + +def test_google_decodes_media_data_uri_exactly_once(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "audio_url", "audio_url": "data:audio/wav;base64,YWJj"} + ], + } + ], + None, + None, + ) + + blob = contents[0].parts[0].inline_data + assert blob.mime_type == "audio/wav" + assert blob.data == b"abc" + + +def test_google_tool_call_message_with_null_content_does_not_emit_text(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{}"}, + } + ], + } + ], + None, + None, + ) + + assert len(contents[0].parts) == 1 + assert contents[0].parts[0].function_call.name == "lookup" + + +def test_google_rejects_malformed_function_tool_choice(): + with pytest.raises(ValueError, match="function.name"): + GoogleProvider._convert_tool_choice( + {"type": "function", "function": {}} + ) + + +def test_anthropic_merges_parallel_tool_results_into_one_user_turn(): + converted = AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [ + {"id": "call-1", "function": {"name": "lookup_order", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "lookup_customer", "arguments": "{}"}}, + ], + }, + {"role": "tool", "tool_call_id": "call-1", "content": '{"status":"paid"}'}, + {"role": "tool", "tool_call_id": "call-2", "content": '{"name":"Ada"}'}, + ] + ) + + assert [message["role"] for message in converted] == ["assistant", "user"] + assert [block["tool_use_id"] for block in converted[1]["content"]] == ["call-1", "call-2"] + + +def test_volcengine_uses_ark_endpoint_and_ak_sk(monkeypatch): + settings = SimpleNamespace( + volc_access_key="access", + volc_secret_key="secret", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + + provider = VolcengineProvider("doubao-model") + + assert provider._client_options() == { + "base_url": "https://ark.example/v3", + "ak": "access", + "sk": "secret", + } + assert provider._build_chat_request("hello", None, TOOLS, 0.4, False) == { + "model": "doubao-model", + "messages": [{"role": "user", "content": "hello"}], + "temperature": 0.4, + "stream": False, + "tools": TOOLS, + } + + +def _responses_provider() -> OpenAICompatibleProvider: + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "responses-model" + provider._provider = "openai" + provider._protocol = "responses" + return provider + + +def test_responses_request_converts_chat_tools_and_sets_strict_default(): + provider = _responses_provider() + + assert provider._build_responses_request("hello", "system", TOOLS, 0.4, True) == { + "model": "responses-model", + "input": "hello", + "instructions": "system", + "temperature": 0.4, + "stream": True, + "tools": [ + { + "type": "function", + "name": "lookup_order", + "description": "查询订单", + "parameters": TOOLS[0]["function"]["parameters"], + "strict": False, + } + ], + } + + +def test_responses_request_omits_implicit_temperature(): + provider = _responses_provider() + + request = provider._build_responses_request("hello", stream=False) + + assert "temperature" not in request + + +def test_responses_request_preserves_configured_temperature(): + provider = _responses_provider() + provider._options = {"temperature": 0.2} + + request = provider._build_responses_request("hello", stream=False) + + assert request["temperature"] == 0.2 + + +def _tool_call_history(): + return [ + {"role": "user", "content": "查订单"}, + { + "role": "assistant", + "content": "我来查询。", + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + }, + { + "id": "call-2", + "type": "function", + "function": { + "name": "lookup_customer", + "arguments": {"customer_id": "C-1"}, + }, + }, + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"status":"paid"}', + }, + { + "role": "tool", + "tool_call_id": "call-2", + "content": {"name": "Ada"}, + }, + ] + + +def test_responses_input_converts_chat_tool_messages_and_parallel_calls(): + converted = normalize_responses_input(None, None, _tool_call_history()) + + assert converted == [ + {"role": "user", "content": "查订单"}, + {"role": "assistant", "content": "我来查询。"}, + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + { + "type": "function_call", + "call_id": "call-2", + "name": "lookup_customer", + "arguments": '{"customer_id":"C-1"}', + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": '{"status":"paid"}', + }, + { + "type": "function_call_output", + "call_id": "call-2", + "output": '{"name":"Ada"}', + }, + ] + + +def test_responses_input_keeps_prompt_string_and_validates_native_items(): + assert normalize_responses_input("hello", "system", None) == "hello" + assert normalize_responses_input( + None, + None, + [ + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup", + "arguments": "{}", + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": "done", + }, + ], + ) == [ + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup", + "arguments": "{}", + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": "done", + }, + ] + + +def test_responses_input_converts_chat_multimodal_content_blocks(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "text", "text": "请阅读图片和文件。"}, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png", + "detail": "high", + }, + }, + { + "type": "file", + "file": { + "file_id": "file-1", + "filename": "说明.txt", + }, + }, + ], + } + ], + ) + + assert converted == [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "请阅读图片和文件。"}, + { + "type": "input_image", + "detail": "high", + "image_url": "https://example.com/image.png", + }, + { + "type": "input_file", + "file_id": "file-1", + "filename": "说明.txt", + }, + ], + } + ] + + from openai.types.responses.response_input_item_param import ResponseInputItemParam + from pydantic import TypeAdapter + + TypeAdapter(list[ResponseInputItemParam]).validate_python(converted) + + +def test_responses_input_keeps_native_multimodal_items_in_sdk_shape(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "hello"}, + {"type": "input_image", "file_id": "file-2"}, + {"type": "input_file", "file_url": "https://example.com/a.pdf"}, + ], + } + ], + ) + + assert converted[0]["content"] == [ + {"type": "input_text", "text": "hello"}, + {"type": "input_image", "detail": "auto", "file_id": "file-2"}, + {"type": "input_file", "file_url": "https://example.com/a.pdf"}, + ] + + +def test_responses_input_converts_previous_output_text_for_follow_up_messages(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "content": [{"type": "output_text", "text": "上一次答复"}], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": [ + {"type": "text", "text": "工具输出"}, + {"type": "file", "url": "https://example.com/result.txt"}, + ], + }, + ], + ) + + assert converted == [ + { + "role": "assistant", + "content": [{"type": "input_text", "text": "上一次答复"}], + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": [ + {"type": "input_text", "text": "工具输出"}, + { + "type": "input_file", + "file_url": "https://example.com/result.txt", + }, + ], + }, + ] + + +@pytest.mark.parametrize( + "content", + [ + [{"type": "audio_url", "audio_url": "https://example.com/audio.wav"}], + [{"type": "video_url", "video_url": "https://example.com/video.mp4"}], + [{"type": "unknown_part", "value": "x"}], + [{"type": "image_url", "image_url": {}}], + [{"type": "input_file"}], + ], +) +def test_responses_input_rejects_unsupported_or_incomplete_content_blocks(content): + with pytest.raises(ValueError, match="(不受 Responses SDK 支持|缺少)"): + normalize_responses_input( + None, + None, + [{"role": "user", "content": content}], + ) + + +@pytest.mark.parametrize( + "messages", + [ + [{"role": "tool", "content": "missing id"}], + [{"role": "assistant", "tool_calls": [{"function": {"arguments": "{}"}}]}], + [{"role": "assistant", "tool_calls": [{"id": "call-1", "function": {"name": "lookup", "arguments": "{"}}]}], + [{"type": "function_call", "id": "item-1", "name": "lookup", "arguments": "{}"}], + ], +) +def test_responses_input_rejects_unlinked_or_malformed_tool_items(messages): + with pytest.raises(ValueError, match="(call_id|有效 JSON|函数名)"): + normalize_responses_input(None, None, messages) + + +def test_openai_responses_request_converts_tool_history(): + provider = _responses_provider() + + request = provider._build_responses_request( + messages=_tool_call_history(), + stream=False, + ) + + assert request["input"][2]["type"] == "function_call" + assert request["input"][2]["call_id"] == "call-1" + assert request["input"][4] == { + "type": "function_call_output", + "call_id": "call-1", + "output": '{"status":"paid"}', + } + + +def test_openai_responses_token_count_converts_tool_history(): + provider = _responses_provider() + params = provider._build_token_count_request( + CompletionRequest(messages=_tool_call_history(), system_prompt=None) + ) + + assert params["input"][2]["type"] == "function_call" + assert params["input"][4]["type"] == "function_call_output" + + +def test_openai_responses_token_count_converts_multimodal_content(): + provider = _responses_provider() + params = provider._build_token_count_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + system_prompt=None, + ) + ) + + assert params["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + +def test_openai_responses_request_converts_multimodal_content(): + provider = _responses_provider() + request = provider._build_responses_request( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + stream=False, + ) + + assert request["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + +def test_ark_responses_request_converts_tool_history(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest(messages=_tool_call_history(), system_prompt=None, stream=False) + ) + + assert request["input"][2]["type"] == "function_call" + assert request["input"][2]["call_id"] == "call-1" + assert request["input"][4]["type"] == "function_call_output" + + +def test_ark_responses_request_omits_implicit_temperature(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + assert "temperature" not in request + + +def test_ark_responses_request_preserves_configured_temperature(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "server_verified_protocols": ["responses"], + "temperature": 0.2, + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + assert request["temperature"] == 0.2 + + +def test_ark_responses_request_converts_multimodal_content(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + system_prompt=None, + stream=False, + ) + ) + + assert request["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + request = provider._build_responses_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + {"type": "input_video", "file_id": "file-video", "fps": 1.5}, + ], + } + ], + system_prompt=None, + stream=False, + ) + ) + assert request["input"][0]["content"] == [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + {"type": "input_video", "file_id": "file-video", "fps": 1.5}, + ] + + +def test_ark_responses_input_supports_audio_video_and_image_pixel_limit(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + { + "type": "audio_url", + "audio_url": {"url": "https://example.com/a.wav"}, + }, + { + "type": "video_url", + "video_url": "https://example.com/v.mp4", + "fps": 2.0, + }, + { + "type": "image_url", + "image_url": "https://example.com/i.png", + "detail": "high", + "image_pixel_limit": { + "min_pixels": 1024, + "max_pixels": 4096, + }, + }, + ], + } + ], + provider="ark", + ) + + assert converted[0]["content"] == [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + { + "type": "input_video", + "video_url": "https://example.com/v.mp4", + "fps": 2.0, + }, + { + "type": "input_image", + "detail": "high", + "image_url": "https://example.com/i.png", + "image_pixel_limit": {"min_pixels": 1024, "max_pixels": 4096}, + }, + ] + + from pydantic import TypeAdapter + from volcenginesdkarkruntime.types.responses.response_input_param import ( + ResponseInputParam, + ) + + TypeAdapter(ResponseInputParam).validate_python(converted) + + +def test_openai_responses_rejects_ark_only_multimodal_content(): + with pytest.raises(ValueError, match="不受 Responses SDK 支持"): + normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"} + ], + } + ], + ) + + +def test_responses_request_converts_chat_tool_choice_to_flat_function_shape(): + provider = _responses_provider() + + request = provider._build_responses_request( + "hello", + tools=TOOLS, + tool_choice={"type": "function", "function": {"name": "lookup_order"}}, + stream=False, + ) + + assert request["tool_choice"] == {"type": "function", "name": "lookup_order"} + + +def test_responses_request_preserves_native_tool_choice_forms(): + provider = _responses_provider() + + assert provider._build_responses_request("hello", tool_choice="required", stream=False)[ + "tool_choice" + ] == "required" + assert provider._build_responses_request( + "hello", tool_choice={"type": "allowed_tools", "mode": "auto"}, stream=False + )["tool_choice"] == {"type": "allowed_tools", "mode": "auto"} + + +def test_responses_rejects_malformed_tool_definitions(): + provider = _responses_provider() + with pytest.raises(ValueError, match="工具定义"): + provider._build_responses_request("hello", tools=[None], stream=False) + with pytest.raises(ValueError, match="function"): + provider._build_responses_request( + "hello", tools=[{"type": "function", "function": "bad"}], stream=False + ) + + +def test_ark_model_sampling_options_merge_into_extra_body(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "server_verified_protocols": ["responses"], + "top_k": 20, + "seed": 7, + "extra_body": {"vendor_flag": True}, + } + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False, top_k=30) + ) + assert request["extra_body"] == {"vendor_flag": True, "top_k": 30, "seed": 7} + assert "server_verified_protocols" not in request + + +def test_openai_responses_rejects_conflicting_model_length_aliases(): + provider = _responses_provider() + provider._options = {"max_tokens": 64, "max_completion_tokens": 128} + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_responses_request("hello", stream=False) + + +def test_ark_responses_rejects_conflicting_model_length_aliases(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "max_tokens": 64, + "max_completion_tokens": 128, + } + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + +def test_ark_extra_body_sampling_conflicts_are_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"extra_body": {"seed": 1}} + with pytest.raises(ValueError, match="seed.*重复"): + provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False, seed=2) + ) + + +def test_responses_sync_non_stream_extracts_output_text(): + provider = _responses_provider() + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": False})) == ["答复"] + assert calls == [{"model": "responses-model", "input": "hello", "stream": False}] + + +def test_responses_complete_uses_response_status_as_finish_reason(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + id="resp-1", + status="completed", + output_text="答复", + usage=SimpleNamespace(input_tokens=2, output_tokens=3, total_tokens=5), + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.text == "答复" + assert result.finish_reason == "completed" + assert result.response_id == "resp-1" + assert result.usage["total_tokens"] == 5 + + +def test_openai_responses_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = _responses_provider() + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + if request["stream"]: + return iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.complete( + CompletionRequest(messages=history, system_prompt=None) + ).text == "答复" + list(provider.stream_events(CompletionRequest(messages=history, system_prompt=None))) + + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[0]["input"][4]["type"] == "function_call_output" + assert calls[1]["input"] == calls[0]["input"] + + +def test_openai_responses_async_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = _responses_provider() + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + if request["stream"]: + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + result = await provider.acomplete( + CompletionRequest(messages=history, system_prompt=None) + ) + events = [ + event + async for event in provider.astream_events( + CompletionRequest(messages=history, system_prompt=None) + ) + ] + return result, events + + result, events = asyncio.run(collect()) + + assert result.text == "异步答复" + assert events[-1].type == "finish" + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[1]["input"][4]["type"] == "function_call_output" + + +def test_openai_responses_async_stream_drains_after_completion(): + provider = _responses_provider() + streams = [] + + class Responses: + async def create(self, **_request): + class Stream: + def __init__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + SimpleNamespace(type="response.output_text.done"), + ] + ) + self.exhausted = False + self.closed = False + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + self.exhausted = True + raise StopAsyncIteration from exc + + async def close(self): + self.closed = True + + stream = Stream() + streams.append(stream) + return stream + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [ + event + async for event in provider.astream_events( + CompletionRequest(prompt="hello") + ) + ] + + events = asyncio.run(collect()) + + assert [event.type for event in events] == ["finish"] + assert streams[0].exhausted is True + assert streams[0].closed is True + + +def test_ark_responses_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + if request["stream"]: + return iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.complete( + CompletionRequest(messages=history, system_prompt=None) + ).text == "答复" + list(provider.stream_events(CompletionRequest(messages=history, system_prompt=None))) + + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[0]["input"][4]["type"] == "function_call_output" + + +def test_ark_responses_async_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + if request["stream"]: + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + result = await provider.acomplete( + CompletionRequest(messages=history, system_prompt=None) + ) + events = [ + event + async for event in provider.astream_events( + CompletionRequest(messages=history, system_prompt=None) + ) + ] + return result, events + + result, events = asyncio.run(collect()) + + assert result.text == "异步答复" + assert events[-1].type == "finish" + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[1]["input"][4]["type"] == "function_call_output" + + +def test_openai_chat_stream_events_include_tool_usage_and_finish(): + provider = OpenAICompatibleProvider.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", tool_calls=None), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", + index=0, + function=SimpleNamespace(name="lookup", arguments='{"id":'), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id=None, + index=0, + function=SimpleNamespace(name=None, arguments='1}'), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[SimpleNamespace(delta=SimpleNamespace(content=None), finish_reason="tool_calls")], + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=3, total_tokens=5), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "text_delta", "tool_call_delta", "tool_call_delta", "tool_call_completed", "finish" + ] + assert events[1].tool_call["name"] == "lookup" + assert events[3].tool_call["arguments"] == '{"id":1}' + assert events[4].finish_reason == "tool_calls" + assert events[4].usage["total_tokens"] == 5 + + +def test_openai_chat_stream_events_preserve_text_and_finish_from_same_chunk(): + events = OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content=None, refusal=None, tool_calls=None), + finish_reason="stop", + ) + ], + ) + ) + + assert [event.type for event in events] == ["text_delta", "finish"] + assert events[0].text == "答" + assert events[1].finish_reason == "stop" + + +def test_openai_chat_stream_error_event_is_explicit(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="chat upstream failed", status=400), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + with pytest.raises(RuntimeError, match="chat upstream failed"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + with pytest.raises(RuntimeError, match="chat upstream failed"): + OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace(error=SimpleNamespace(message="chat upstream failed")) + ) + + +def test_ark_chat_stream_error_event_is_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._provider = "volcengine" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="ark upstream failed", status=400), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + with pytest.raises(RuntimeError, match="ark upstream failed"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + with pytest.raises(RuntimeError, match="ark upstream failed"): + VolcengineProvider._chat_stream_events( + SimpleNamespace(error=SimpleNamespace(message="ark upstream failed")) + ) + + +def test_openai_async_chat_stream_error_event_is_explicit(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="async chat upstream failed", status=400), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="async chat upstream failed"): + asyncio.run(collect()) + + +def test_ark_async_chat_stream_error_event_is_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="async ark upstream failed", status=400), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="async ark upstream failed"): + asyncio.run(collect()) + + +def test_ark_chat_stream_events_preserve_text_and_finish_from_same_chunk(): + events = VolcengineProvider._chat_stream_events( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content=None, tool_calls=None), + finish_reason="stop", + ) + ], + ) + ) + + assert [event.type for event in events] == ["text_delta", "finish"] + + +def test_openai_chat_stream_events_propagate_refusal_delta(): + events = OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=None, refusal="不能处理"), + finish_reason=None, + ) + ] + ) + ) + + assert len(events) == 1 + assert events[0].type == "refusal_delta" + assert events[0].refusal == "不能处理" + + +def test_openai_chat_stream_events_preserve_parallel_tool_calls(): + chunk = SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", index=0, + function=SimpleNamespace(name="lookup", arguments='{"id":'), + ), + SimpleNamespace( + id="call-2", index=1, + function=SimpleNamespace(name="refund", arguments='{"order":'), + ), + ], + ), + finish_reason=None, + ) + ], + ) + + events = OpenAICompatibleProvider._chat_stream_events(chunk) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_delta"] + assert [event.tool_call["id"] for event in events] == ["call-1", "call-2"] + assert [event.tool_call["index"] for event in events] == [0, 1] + + +def test_openai_responses_stream_exposes_tool_delta_and_completed_events(): + assert OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.output_item.added", + item=SimpleNamespace(type="function_call", call_id="call-1", name="lookup"), + ) + ) is None + + delta = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=2, + delta='{"id":', + ) + ) + completed = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=2, + item=SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ), + ) + ) + + assert delta.type == "tool_call_delta" + assert delta.tool_call["index"] == 2 + assert completed.type == "tool_call_completed" + assert completed.tool_call["id"] == "call-1" + assert completed.tool_call["arguments"] == '{"id":1}' + + +def test_openai_responses_stream_deduplicates_tool_completion_events(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ) + return iter( + [ + SimpleNamespace(type="response.output_item.done", item=item, output_index=0), + SimpleNamespace( + type="response.function_call_arguments.done", + item=item, + output_index=0, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_completed", "finish"] + + +def test_openai_responses_stream_flushes_tool_delta_before_finish(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + name="lookup", + delta='{"id":1}', + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + + +def test_ark_responses_stream_flushes_tool_delta_before_finish(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + name="lookup", + delta='{"id":1}', + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + + +def test_ark_responses_stream_deduplicates_tool_completion_events(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ) + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + delta='{"id":', + ), + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=0, + name="lookup", + arguments='{"id":1}', + ), + SimpleNamespace( + type="response.output_item.done", + item=item, + output_index=0, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + assert events[1].tool_call["id"] == "call-1" + assert events[1].tool_call["arguments"] == '{"id":1}' + + +def test_openai_responses_arguments_done_is_complete_without_output_item(): + event = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=2, + name="lookup", + arguments='{"id":1}', + ) + ) + + assert event is not None + assert event.type == "tool_call_completed" + assert event.tool_call == { + "id": "item-1", + "index": 2, + "type": "function_call", + "name": "lookup", + "arguments": '{"id":1}', + } + + +def test_openai_responses_custom_tool_stream_keeps_added_metadata_and_raw_input(): + metadata = {} + added = SimpleNamespace( + type="response.output_item.added", + output_index=1, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ) + + assert OpenAICompatibleProvider._responses_stream_event(added, metadata) is None + delta = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=1, + delta="echo ", + ), + metadata, + ) + completed = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=1, + input="hello world", + ), + metadata, + ) + + assert delta is not None and delta.tool_call == { + "id": "item-1", + "call_id": "call-1", + "index": 1, + "type": "custom_tool_call", + "name": "run_command", + "arguments": "echo ", + } + assert completed is not None and completed.tool_call == { + "id": "item-1", + "index": 1, + "type": "custom_tool_call", + "name": "run_command", + "arguments": "hello world", + "call_id": "call-1", + } + + +def test_openai_responses_custom_tool_stream_events_emit_non_json_completion(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="echo hello", + ) + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo ", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.output_item.done", + output_index=0, + item=item, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[0].tool_call["name"] == "run_command" + assert events[1].tool_call["arguments"] == "echo hello" + assert events[1].tool_call["type"] == "custom_tool_call" + + +def test_openai_responses_custom_tool_empty_input_is_valid(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="ping", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert events[0].type == "tool_call_completed" + assert events[0].tool_call["arguments"] == "" + + +def test_openai_responses_custom_tool_async_stream_preserves_metadata(): + provider = _responses_provider() + + class Responses: + async def create(self, **_request): + events = [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo hello", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + + class Stream: + def __aiter__(self): + self._events = iter(events) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + events = asyncio.run(collect()) + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[1].tool_call["type"] == "custom_tool_call" + assert events[1].tool_call["name"] == "run_command" + + +def test_google_response_text_excludes_thought_parts(): + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + content=SimpleNamespace( + parts=[ + SimpleNamespace(text="思考", thought=True), + SimpleNamespace(text="答案", thought=False), + ] + ) + ) + ] + ) + + assert GoogleProvider._response_text(response) == "答案" + + +def test_anthropic_tool_start_arguments_use_string_contract(): + event = AnthropicProvider._stream_event( + SimpleNamespace( + type="content_block_start", + index=0, + content_block=SimpleNamespace( + type="tool_use", id="toolu-1", name="lookup", input={"id": 1} + ), + ), + "msg-1", + ) + + assert event is not None + assert event.tool_call["arguments"] == '{"id":1}' + + +def test_anthropic_user_profile_adds_required_beta_header(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + params = provider._build_message_params(prompt="hi", user="profile-1") + + assert params["user_profile_id"] == "profile-1" + assert params["extra_headers"]["anthropic-beta"] == "user-profiles-2026-08-18" + + +def test_anthropic_stream_deduplicates_message_finish_events(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class StreamManager: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="message_delta", + delta=SimpleNamespace(stop_reason="end_turn"), + usage=SimpleNamespace(output_tokens=1), + ), + SimpleNamespace(type="message_stop"), + ] + ) + + class Messages: + def stream(self, **_kwargs): + return StreamManager() + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["finish"] + + +def test_anthropic_non_refusal_stop_details_do_not_create_refusal_result(): + response = SimpleNamespace( + content=[], + stop_reason="end_turn", + stop_details=SimpleNamespace( + type="max_tokens", + explanation="normal completion detail", + ), + ) + + result = AnthropicProvider._extract_result(response) + + assert result.refusal is None + assert result.finish_reason == "end_turn" + + +def test_anthropic_non_refusal_stream_stop_details_emit_finish(): + event = SimpleNamespace( + type="message_delta", + delta=SimpleNamespace( + stop_reason="end_turn", + stop_details=SimpleNamespace( + type="max_tokens", + explanation="normal completion detail", + ), + ), + usage=None, + ) + + converted = AnthropicProvider._stream_event(event, "msg-1") + + assert converted is not None + assert converted.type == "finish" + assert converted.finish_reason == "end_turn" + assert converted.refusal is None + + +def test_anthropic_invoke_retries_stream_context_before_first_event(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + attempts = [] + + class StreamManager: + def __init__(self, fail_on_enter): + self.fail_on_enter = fail_on_enter + self.text_stream = iter(["答复"]) + + def __enter__(self): + if self.fail_on_enter: + raise RuntimeError("transient stream setup failure") + return self + + def __exit__(self, *_args): + return False + + class Messages: + def stream(self, **_kwargs): + attempts.append(True) + return StreamManager(len(attempts) == 1) + + def fake_retry(call): + last_error = None + for _ in range(2): + try: + return call() + except RuntimeError as error: + last_error = error + raise last_error + + monkeypatch.setattr(model_provider_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + + assert list(provider.invoke("hello", stream=True)) == ["答复"] + assert len(attempts) == 2 + + +def test_anthropic_ainvoke_retries_async_stream_context_before_first_event(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + attempts = [] + + class AsyncStreamManager: + def __init__(self, fail_on_enter): + self.fail_on_enter = fail_on_enter + self.text_stream = ["异步答复"] + + async def __aenter__(self): + if self.fail_on_enter: + raise RuntimeError("transient async stream setup failure") + return self + + async def __aexit__(self, *_args): + return False + + class Messages: + def stream(self, **_kwargs): + attempts.append(True) + return AsyncStreamManager(len(attempts) == 1) + + async def fake_retry(call): + last_error = None + for _ in range(2): + try: + result = call() + return await result if inspect.isawaitable(result) else result + except RuntimeError as error: + last_error = error + raise last_error + + monkeypatch.setattr(model_provider_module, "retry_async_call", fake_retry) + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + assert asyncio.run(consume()) == ["异步答复"] + assert len(attempts) == 2 + + +def test_anthropic_invoke_non_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + response = SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="不能回答")], + stop_reason="refusal", + ) + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace(create=lambda **_kwargs: response) + ) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + list(provider.invoke("hello", stream=False)) + + +def test_anthropic_invoke_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class StreamManager: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="不能回答"), + ), + SimpleNamespace(type="message_stop"), + ] + ) + + text_stream = iter(()) + + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace(stream=lambda **_kwargs: StreamManager()) + ) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + list(provider.invoke("hello", stream=True)) + + +def test_anthropic_text_stream_detects_refusal_before_consuming_text_stream(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class RealLikeStream: + current_message_snapshot = property( + lambda _self: SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="不能回答")], + stop_reason="refusal", + ) + ) + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="不能回答"), + ) + ] + ) + + text_stream = iter(["这段文本不应先返回"]) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + next(AnthropicProvider._iter_invoke_stream_text(RealLikeStream())) + + +def test_anthropic_text_stream_checks_snapshot_before_yielding_text(): + class SnapshotStream: + current_message_snapshot = property( + lambda _self: SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="拒答")], + stop_reason="refusal", + ) + ) + text_stream = iter(["不应输出"]) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + next(AnthropicProvider._iter_invoke_stream_text(SnapshotStream())) + + +def test_anthropic_stream_error_preserves_nested_error_message(): + event = { + "type": "error", + "error": { + "type": "invalid_request_error", + "error": {"message": "nested request detail"}, + }, + } + + with pytest.raises(RuntimeError, match="nested request detail"): + AnthropicProvider._stream_event(event) + + +def test_openai_responses_non_stream_reasoning_summary_list_is_preserved(): + response = SimpleNamespace( + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="先分析"), SimpleNamespace(text="后结论")], + ) + ] + ) + + assert OpenAICompatibleProvider._extract_reasoning(response) == "先分析后结论" + + +def test_ark_responses_non_stream_reasoning_summary_list_is_preserved(): + response = SimpleNamespace( + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="Ark 思考")], + ) + ], + choices=[], + output_text="", + usage=None, + status="completed", + id="resp-1", + ) + + assert VolcengineProvider._extract_result(response).reasoning == "Ark 思考" + + +def test_anthropic_ainvoke_non_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + response = SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="异步不能回答")], + stop_reason="refusal", + ) + + class Messages: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + asyncio.run(consume()) + + +def test_anthropic_ainvoke_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class AsyncStreamManager: + async def __aenter__(self): + return self + + async def __aexit__(self, *_args): + return False + + def __aiter__(self): + async def events(): + yield SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="异步不能回答"), + ) + yield SimpleNamespace(type="message_stop") + + return events() + + text_stream = () + + provider._get_aclient = lambda: SimpleNamespace( + messages=SimpleNamespace(stream=lambda **_kwargs: AsyncStreamManager()) + ) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + asyncio.run(consume()) + + +def test_responses_invoke_rejects_tools_for_text_only_interface(): + provider = _responses_provider() + provider._get_client = lambda: pytest.fail("Responses invoke 不应初始化客户端") + + with pytest.raises(ValueError, match="tools|工具"): + list(provider.invoke("hello", tools=TOOLS)) + + +def test_responses_ainvoke_rejects_tools_for_text_only_interface(): + provider = _responses_provider() + provider._get_aclient = lambda: pytest.fail("Responses ainvoke 不应初始化客户端") + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", tools=TOOLS)] + + with pytest.raises(ValueError, match="tools|工具"): + asyncio.run(consume()) + + +def test_chat_invoke_accepts_mapping_shaped_sdk_response(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = {"choices": [{"message": {"content": "字典答复"}}]} + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: response) + ) + ) + + assert list(provider.invoke("hello", stream=False)) == ["字典答复"] + + +def test_chat_ainvoke_accepts_mapping_shaped_sdk_response(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = {"choices": [{"message": {"content": "异步字典答复"}}]} + + class Completions: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["异步字典答复"] + + +def test_openai_chat_business_error_is_explicit_for_complete_and_invoke(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: response) + ) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + provider.complete(CompletionRequest(prompt="hello")) + with pytest.raises(RuntimeError, match="435.*Model not support"): + list(provider.invoke("hello", stream=False)) + + +def test_openai_chat_business_error_is_explicit_for_async_complete_and_invoke(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + + class Completions: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(consume()) + + +def test_openai_chat_business_error_is_explicit_for_sync_and_async_streams(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: iter([response])) + ) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + class AsyncCompletions: + async def create(self, **_kwargs): + class Stream: + def __aiter__(self): + self._events = iter([response]) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=AsyncCompletions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(collect()) + + +def test_ark_responses_invoke_rejects_tools_for_text_only_interface(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + provider._get_client = lambda: pytest.fail("Ark Responses invoke 不应初始化客户端") + + with pytest.raises(ValueError, match="tools|工具"): + list(provider.invoke("hello", tools=TOOLS)) + + +def test_ark_responses_ainvoke_rejects_tools_for_text_only_interface(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + provider._get_aclient = lambda: pytest.fail("Ark Responses ainvoke 不应初始化客户端") + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", tools=TOOLS)] + + with pytest.raises(ValueError, match="tools|工具"): + asyncio.run(consume()) + + +@pytest.mark.parametrize( + "method_name", + [ + "retrieve_response", + "create_response", + "delete_response", + "list_response_input_items", + "list_input_items", + ], +) +def test_ark_response_resources_require_responses_protocol(method_name): + provider = object.__new__(VolcengineProvider) + provider._protocol = "chat_completions" + provider._get_client = lambda: pytest.fail("Ark Responses 资源不应初始化客户端") + method = getattr(provider, method_name) + + args = { + "retrieve_response": ("resp-1",), + "create_response": (), + "delete_response": ("resp-1",), + "list_response_input_items": ("resp-1",), + "list_input_items": ("resp-1",), + }[method_name] + kwargs = {"input": "hello"} if method_name == "create_response" else {} + + with pytest.raises(ValueError, match="仅适用于 responses 协议"): + method(*args, **kwargs) + + +@pytest.mark.parametrize( + "method_name", + [ + "async_retrieve_response", + "async_create_response", + "async_delete_response", + "async_list_response_input_items", + "async_list_input_items", + ], +) +def test_ark_async_response_resources_require_responses_protocol(method_name): + provider = object.__new__(VolcengineProvider) + provider._protocol = "chat_completions" + provider._get_aclient = lambda: pytest.fail("Ark Async Responses 资源不应初始化客户端") + method = getattr(provider, method_name) + + args = { + "async_retrieve_response": ("resp-1",), + "async_create_response": (), + "async_delete_response": ("resp-1",), + "async_list_response_input_items": ("resp-1",), + "async_list_input_items": ("resp-1",), + }[method_name] + kwargs = {"input": "hello"} if method_name == "async_create_response" else {} + + async def invoke(): + await method(*args, **kwargs) + + with pytest.raises(ValueError, match="仅适用于 responses 协议"): + asyncio.run(invoke()) + + +def test_openai_complete_uses_retry_call_wrapper(monkeypatch): + provider = _responses_provider() + provider._provider = "openai" + provider._base_url = None + calls = [] + + def fake_retry(call): + calls.append(call) + return call() + + monkeypatch.setattr(openai_compatible_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace( + create=lambda **_request: SimpleNamespace(status="completed", output_text="答复") + ) + ) + + assert provider.complete(CompletionRequest(prompt="hello")).text == "答复" + assert len(calls) == 1 + + +def test_openai_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = _responses_provider() + provider._provider = "openai" + provider._base_url = None + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(openai_compatible_module, "retry_async_call", fake_retry) + + class Responses: + async def create(self, **_request): + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_anthropic_complete_uses_retry_call_wrapper(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + calls = [] + + def fake_retry(call): + calls.append(call) + return call() + + monkeypatch.setattr(anthropic_provider_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace( + create=lambda **_request: SimpleNamespace( + content=[SimpleNamespace(type="text", text="答复")] + ) + ) + ) + + assert provider.complete(CompletionRequest(prompt="hello")).text == "答复" + assert len(calls) == 1 + + +def test_anthropic_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(anthropic_provider_module, "retry_async_call", fake_retry) + + class Messages: + async def create(self, **_request): + return SimpleNamespace(content=[SimpleNamespace(type="text", text="异步答复")]) + + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_ark_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-test" + provider._protocol = "chat_completions" + provider._options = {} + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(volcengine_module, "retry_async_call", fake_retry) + + class Completions: + async def create(self, **_request): + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="异步答复", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_openai_and_ark_embedding_base64_is_decoded_to_float_vectors(): + encoded = base64.b64encode(struct.pack("<2f", 1.25, -2.5)).decode("ascii") + + openai_provider = object.__new__(OpenAICompatibleProvider) + openai_provider._model_name = "embedding" + openai_provider._provider = "openai" + openai_provider._options = {} + openai_provider._client = None + openai_provider._get_client = lambda: SimpleNamespace( + embeddings=SimpleNamespace( + create=lambda **_kwargs: SimpleNamespace( + data=[SimpleNamespace(embedding=encoded)] + ) + ) + ) + assert openai_provider.embed_documents(["text"], encoding_format="base64") == [[1.25, -2.5]] + + ark_provider = object.__new__(VolcengineProvider) + ark_provider._model_name = "embedding" + ark_provider._options = {} + ark_provider._get_client = lambda: SimpleNamespace( + embeddings=SimpleNamespace( + create=lambda **_kwargs: SimpleNamespace( + data=[SimpleNamespace(embedding=encoded)] + ) + ) + ) + assert ark_provider.embed_documents(["text"], encoding_format="base64") == [[1.25, -2.5]] + + +def test_responses_async_non_stream_extracts_output_text(): + provider = _responses_provider() + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [chunk async for chunk in provider._ainvoke_responses({"model": "responses-model", "input": "hello", "stream": False})] + + assert asyncio.run(collect()) == ["异步答复"] + assert calls == [{"model": "responses-model", "input": "hello", "stream": False}] + + +def test_responses_sync_stream_yields_text_and_refusal_deltas(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + assert request["stream"] is True + return iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="好"), + SimpleNamespace(type="response.refusal.delta", delta="的"), + SimpleNamespace(type="response.completed"), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="拒答"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_sync_non_stream_refusal_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + status="completed", + output=[ + SimpleNamespace( + type="message", + content=[SimpleNamespace(type="refusal", refusal="不能回答")], + ) + ], + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="拒答"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": False})) + + +def test_responses_async_stream_yields_text_deltas(): + provider = _responses_provider() + + class AsyncEvents: + def __aiter__(self): + return self + + async def __anext__(self): + if not hasattr(self, "events"): + self.events = iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="异"), + SimpleNamespace(type="response.output_text.delta", delta="步"), + SimpleNamespace(type="response.completed"), + ] + ) + try: + return next(self.events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + class Responses: + async def create(self, **request): + assert request["stream"] is True + return AsyncEvents() + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [chunk async for chunk in provider._ainvoke_responses({"model": "responses-model", "input": "hello", "stream": True})] + + assert asyncio.run(collect()) == ["异", "步"] + + +@pytest.mark.parametrize( + ("status", "details", "expected"), + [ + ("failed", SimpleNamespace(), "Responses API 响应状态为 failed"), + ("incomplete", SimpleNamespace(reason="max_output_tokens"), "max_output_tokens"), + ], +) +def test_responses_non_stream_terminal_status_is_explicit_error(status, details, expected): + provider = _responses_provider() + response = SimpleNamespace(status=status, incomplete_details=details, error=None) + + class Responses: + def create(self, **request): + return response + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match=expected): + list( + provider._invoke_responses( + {"model": "responses-model", "input": "hello", "stream": False} + ) + ) + + +def test_responses_stream_failed_event_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + return iter( + [ + SimpleNamespace( + type="response.failed", + response=SimpleNamespace( + status="failed", + error=SimpleNamespace(message="upstream failed"), + ), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="upstream failed"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_stream_incomplete_event_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + return iter( + [ + SimpleNamespace( + type="response.incomplete", + response=SimpleNamespace( + status="incomplete", + incomplete_details=SimpleNamespace(reason="content_filter"), + ), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="content_filter"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_stream_error_event_is_explicit_error(): + with pytest.raises(RuntimeError, match="rate limit"): + OpenAICompatibleProvider._stream_delta( + SimpleNamespace(type="error", message="rate limit") + ) + + +def test_responses_cancelled_events_are_explicit_errors(): + with pytest.raises(RuntimeError, match="cancelled|取消"): + OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="cancelled"), + ) + ) + + with pytest.raises(RuntimeError, match="cancelled|取消"): + OpenAICompatibleProvider._stream_delta( + SimpleNamespace(type="response.cancelled") + ) + + +def test_openai_responses_non_stream_extracts_refusal_output(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + status="completed", + output=[ + SimpleNamespace( + type="message", + content=[SimpleNamespace(type="refusal", refusal="不能回答")], + ) + ], + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.refusal == "不能回答" + + +def test_deepseek_chat_uses_max_tokens_for_explicit_and_configured_limits(): + provider = object.__new__(DeepSeekProvider) + provider._model_name = "deepseek-chat" + provider._protocol = "chat_completions" + + provider._options = {} + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=128) + assert request["max_tokens"] == 128 + assert "max_completion_tokens" not in request + + provider._options = {"max_tokens": 256} + request = provider._build_chat_request(prompt="hi", stream=False) + assert request["max_tokens"] == 256 + assert "max_completion_tokens" not in request diff --git a/tests/providers/test_rerank_contracts.py b/tests/providers/test_rerank_contracts.py new file mode 100644 index 0000000..dfb5536 --- /dev/null +++ b/tests/providers/test_rerank_contracts.py @@ -0,0 +1,104 @@ +import pytest + +from src.providers.jina import JinaProvider +from src.providers.siliconflow_rerank import SiliconflowRerankProvider + + +def test_jina_duplicate_documents_keep_distinct_indices(): + provider = object.__new__(JinaProvider) + indices, scores = provider._parse_response( + [ + {"document": {"text": "same"}, "relevance_score": 0.9}, + {"document": {"text": "same"}, "relevance_score": 0.8}, + ], + ["same", "same"], + ) + assert indices == [0, 1] + assert scores == [0.9, 0.8] + + +def test_siliconflow_duplicate_documents_keep_distinct_indices(): + provider = object.__new__(SiliconflowRerankProvider) + indices, scores = provider._parse_response( + [ + {"document": {"text": "same"}, "relevance_score": 0.9}, + {"document": {"text": "same"}, "relevance_score": 0.8}, + ], + ["same", "same"], + ) + assert indices == [0, 1] + assert scores == [0.9, 0.8] + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_accepts_top_n_larger_than_document_count(provider_type): + provider = object.__new__(provider_type) + + provider._validate_inputs("query", ["document"], 2) + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_invalid_index_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="index 无效"): + provider._parse_response( + [{"index": 4, "relevance_score": 0.9}], + ["one", "two"], + ) + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_unknown_document_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="无法映射"): + provider._parse_response( + [{"document": {"text": "unknown"}, "relevance_score": 0.9}], + ["one", "two"], + ) + + +@pytest.mark.parametrize("score", ["not-a-number", float("nan"), float("inf")]) +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_invalid_score_is_explicit(provider_type, score): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="relevance_score"): + provider._parse_response( + [{"index": 0, "relevance_score": score}], + ["one"], + ) + + +@pytest.mark.parametrize("timeout", [float("nan"), float("inf"), float("-inf"), 0, -1]) +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_non_finite_or_non_positive_timeout_is_explicit(provider_type, timeout): + provider = object.__new__(provider_type) + provider._options = {"timeout": timeout} + + with pytest.raises(ValueError, match="timeout.*正数"): + provider._request_timeout() + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_duplicate_index_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="重复 index"): + provider._parse_response( + [ + {"index": 0, "relevance_score": 0.9}, + {"index": 0, "relevance_score": 0.8}, + ], + ["one"], + ) + + +def test_siliconflow_rerank_rejects_return_documents_override(): + provider = object.__new__(SiliconflowRerankProvider) + provider._model_name = "rerank-model" + provider._options = {"return_documents": False} + + with pytest.raises(ValueError, match="return_documents"): + provider._prepare_payload("query", ["document"], 1) diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py new file mode 100644 index 0000000..e26c4e2 --- /dev/null +++ b/tests/providers/test_sdk_capabilities.py @@ -0,0 +1,5547 @@ +import asyncio +import inspect +import json +from types import SimpleNamespace + +import pytest +from google.auth.credentials import AnonymousCredentials +from google.genai import types + +from src.providers.__base__.model_provider import ( + CompletionRequest, + LargeLanguageModel, + TextEmbeddingModel, +) +from src.providers.anthropic import AnthropicProvider +from src.providers.factory import ModelProviderFactory +from src.providers.google import GoogleProvider +from src.providers.openai import OpenAIProvider +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import AsyncGoogleResources, GoogleResources +from src.providers.volcengine import VolcengineProvider +from src.utils.config import ModelDetail +from src.utils.security import ( + find_sensitive_option_paths, + is_sensitive_option_key, + redact_sensitive_text, + validate_secret_free_options, + validate_secret_free_payload, + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) + + +def test_model_detail_options_are_explicit_and_secret_free(): + detail = ModelDetail(provider="openai", model_name="demo", options={"top_p": 0.8}) + assert detail.options == {"top_p": 0.8} + with pytest.raises(ValueError, match="凭证"): + ModelDetail(provider="openai", model_name="demo", options={"api_key": "secret"}) + with pytest.raises(ValueError, match="凭证"): + ModelDetail(provider="openai", model_name="demo", options={"nested": {"headers": {"Authorization": "x"}}}) + with pytest.raises(ValueError): + ModelDetail(provider="openai", model_name="demo", unknown=True) + + +@pytest.mark.parametrize( + "field_name", + ["extra_headers", "extra_query"], +) +def test_completion_request_rejects_credentials_in_request_overrides(field_name): + with pytest.raises(ValueError, match="请求头/query"): + CompletionRequest(**{field_name: {"Authorization": "Bearer secret"}}) + + +def test_completion_request_keeps_safe_request_overrides_isolated(): + headers = {"X-Trace-ID": "trace-1"} + query = {"tenant": "demo"} + request = CompletionRequest(extra_headers=headers, extra_query=query) + + assert request.extra_headers == headers + assert request.extra_query == query + assert request.extra_headers is not headers + assert request.extra_query is not query + + +def test_completion_request_rejects_credentials_in_extra_body(): + with pytest.raises(ValueError, match="extra_body"): + CompletionRequest(extra_body={"vendor": {"access_token": "secret"}}) + + +def test_google_model_options_allow_safe_http_trace_headers(): + detail = ModelDetail( + provider="google", + model_name="gemini-test", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + assert detail.options["http_options"]["headers"] == {"X-Trace-ID": "trace"} + + +def test_google_model_options_reject_sdk_http_credentials(): + http_options = types.HttpOptions( + headers={"X-API-Key": "secret"}, + ) + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="google", + model_name="gemini-test", + options={"http_options": http_options}, + ) + + +@pytest.mark.parametrize("timeout", ["1", 0, -1, float("nan"), float("inf")]) +def test_google_request_timeout_must_be_finite_and_positive(timeout): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + with pytest.raises(ValueError, match="timeout|正数"): + provider._build_generation_config("system", None, 0.2, timeout=timeout) + + +def test_google_resource_config_rejects_nested_http_credentials(): + with pytest.raises(ValueError, match="凭证"): + GoogleProvider._resource_config( + lambda **values: values, + {"http_options": {"headers": {"Authorization": "Bearer secret"}}}, + {}, + ) + + +def test_google_options_extra_body_rejects_nested_credentials(): + with pytest.raises(ValueError, match="options.extra_body"): + GoogleProvider( + "gemini-test", + options={"extra_body": {"routing": {"access_token": "secret"}}}, + ) + + +@pytest.mark.parametrize("provider", ["openai", "qwen"]) +def test_direct_openai_compatible_provider_rejects_request_header_and_query_options(provider): + settings = SimpleNamespace( + openai_api_key="key", + qwen_api_key="key", + qwen_base_url="https://qwen.invalid/v1", + openai_api_base="https://api.openai.com/v1", + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + provider, + settings=settings, + options={"default_headers": {"X-API-Key": "secret"}}, + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + provider, + settings=settings, + options={"default_query": {"access_token": "secret"}}, + ) + + +def test_direct_openai_compatible_provider_rejects_base_url_and_http_client_options(): + settings = SimpleNamespace( + qwen_api_key="key", + qwen_base_url="https://qwen.invalid/v1", + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + "qwen", + settings=settings, + options={"base_url": "https://proxy.invalid/v1"}, + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + "qwen", + settings=settings, + options={"http_client": object()}, + ) + + +def test_direct_volcengine_provider_rejects_connection_override(monkeypatch): + settings = SimpleNamespace( + volc_access_key="ak", + volc_secret_key="sk", + ark_api_key=None, + volc_base_url="https://ark.invalid/v3", + ) + monkeypatch.setattr("src.providers.volcengine.get_settings", lambda: settings) + with pytest.raises(ValueError, match="请求头/query"): + VolcengineProvider( + "doubao", + options={"base_url": "https://proxy.invalid/v3"}, + ) + + +def test_direct_anthropic_provider_rejects_request_header_and_query_options(monkeypatch): + monkeypatch.setattr( + "src.providers.anthropic.get_settings", + lambda: SimpleNamespace(anthropic_api_key="key"), + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider( + "claude", + options={"default_headers": {"Authorization": "Bearer secret"}}, + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider( + "claude", + options={"default_query": {"access_token": "secret"}}, + ) + + +def test_direct_anthropic_provider_rejects_connection_override(monkeypatch): + monkeypatch.setattr( + "src.providers.anthropic.get_settings", + lambda: SimpleNamespace(anthropic_api_key="key"), + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider("claude", options={"base_url": "https://proxy.invalid"}) + + +def test_openai_chat_moderation_is_forwarded_only_for_official_provider(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + moderation = {"model": "omni-moderation-latest", "policy": {"input": {"mode": "block"}}} + request = provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + assert request["moderation"] == moderation + + provider._provider = "qwen" + with pytest.raises(ValueError, match="moderation"): + provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + + +def test_openai_compatibility_uses_endpoint_not_provider_name_for_official_fields(): + moderation = {"model": "omni-moderation-latest"} + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + provider._provider = "qwen" + provider._base_url = "https://api.openai.com/v1" + request = provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + assert request["moderation"] == moderation + + provider._provider = "openai" + provider._base_url = "https://gateway.example.com/v1" + with pytest.raises(ValueError, match="moderation"): + provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + + +def test_openai_chat_request_supports_messages_and_extended_parameters(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._options = {} + request = provider._build_chat_request( + messages=[{"role": "user", "content": [{"type": "text", "text": "hi"}]}], + temperature=0.2, + max_tokens=128, + top_p=0.9, + seed=7, + response_format={"type": "json_object"}, + stream=False, + ) + assert request["messages"][0]["content"][0]["type"] == "text" + assert request["max_completion_tokens"] == 128 + assert request["seed"] == 7 + assert request["response_format"]["type"] == "json_object" + + +def test_completion_request_omits_implicit_temperature_for_model_defaults(): + request = CompletionRequest(prompt="hi") + assert request.temperature == 0.7 + assert "temperature" not in request.to_invoke_kwargs() + + explicit = CompletionRequest(prompt="hi", temperature=0.7) + assert explicit.to_invoke_kwargs()["temperature"] == 0.7 + + +def test_completion_request_copy_preserves_implicit_temperature_marker(): + request = CompletionRequest(prompt="hi") + copied = request.copy_with(stream=False) + + assert copied.temperature == 0.7 + assert copied.temperature_is_explicit is False + assert "temperature" not in copied.to_invoke_kwargs() + + +def test_openai_model_temperature_remains_the_default_when_request_omits_it(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + + request = CompletionRequest(prompt="hi") + params = provider._build_chat_request(**request.to_invoke_kwargs()) + + assert params["temperature"] == 0.2 + + +def test_google_model_temperature_remains_the_default_when_request_omits_it(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + + request = CompletionRequest(prompt="hi") + config = provider._build_generation_config( + None, + None, + request.temperature if request.temperature_is_explicit else None, + ) + + assert config.temperature == 0.2 + + +def test_legacy_openai_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_openai_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_google_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + calls = [] + + class Models: + def generate_content(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(text="ok") + + client = SimpleNamespace(models=Models()) + provider._get_client = lambda: client + provider._effective_vertex_mode = lambda _client: False + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["config"].temperature == 0.2 + + +def test_legacy_google_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + calls = [] + + class Models: + async def generate_content(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(text="ok") + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + provider._get_client = lambda: client + provider._effective_vertex_mode = lambda _client: False + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["config"].temperature == 0.2 + + +def test_legacy_volcengine_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_volcengine_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_openai_compatible_rejects_conflicting_model_length_aliases(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"max_tokens": 64, "max_completion_tokens": 128} + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_chat_request(prompt="hi", stream=False) + + +def test_openai_chat_extra_body_cannot_override_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {"extra_body": {"temperature": 0.1}} + + with pytest.raises(ValueError, match="extra_body.*temperature"): + provider._build_chat_request(prompt="hi", temperature=0.7, stream=False) + + +def test_openai_request_builder_rejects_sensitive_http_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_chat_request( + prompt="hi", + extra_headers={"X-API-Key": "secret"}, + stream=False, + ) + with pytest.raises(ValueError, match="请求头/query"): + provider._build_responses_request( + prompt="hi", + extra_query={"access_token": "secret"}, + stream=False, + ) + + +def test_openai_resource_calls_fail_before_sdk_on_unknown_or_missing_arguments(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="SDK.*unknown_option"): + provider.create_upload( + bytes=10, + filename="demo.txt", + mime_type="text/plain", + purpose="assistants", + unknown_option=True, + ) + + with pytest.raises(ValueError, match="SDK.*part_ids"): + provider.complete_upload("upload-1") + + +def test_openai_direct_resource_calls_reject_sensitive_http_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_query={"access_token": "secret"}) + + +def test_openai_vector_store_resource_calls_follow_installed_sdk_signature(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="SDK.*unknown_option"): + provider.create_vector_store(name="demo", unknown_option=True) + + +def test_openai_resource_creation_paths_reject_sensitive_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_vector_store, {"extra_headers": {"X-API-Key": "secret"}}), + (provider.create_upload, {"extra_query": {"access_token": "secret"}}), + (provider.complete_upload, {"upload_id": "upload-1", "extra_body": {"token": "secret"}}), + ) + for operation, kwargs in cases: + with pytest.raises(ValueError, match="凭证"): + operation(**kwargs) + + +def test_openai_async_resource_creation_paths_reject_sensitive_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_create_vector_store, {"extra_headers": {"X-API-Key": "secret"}}), + (provider.async_create_upload, {"extra_query": {"access_token": "secret"}}), + (provider.async_complete_upload, {"upload_id": "upload-1", "extra_body": {"token": "secret"}}), + ) + for operation, kwargs in cases: + with pytest.raises(ValueError, match="凭证"): + await operation(**kwargs) + + asyncio.run(run()) + + +def test_openai_resource_groups_validate_unknown_arguments_before_dispatch(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.upload_file, (b"data",), {"purpose": "assistants", "unknown_option": True}), + (provider.create_batch, ("file-1",), {"unknown_option": True}), + (provider.create_conversation, (), {"unknown_option": True}), + (provider.create_fine_tuning_job, (), {"unknown_option": True}), + (provider.create_eval, (), {"unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + operation(*args, **kwargs) + + +def test_openai_resource_missing_required_arguments_are_project_errors(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_fine_tuning_job, (), {}, "model"), + (provider.create_eval, (), {}, "data_source_config"), + (provider.create_video, (), {}, "prompt"), + (provider.update_conversation, ("conversation-1",), {}, "metadata"), + ) + for operation, args, kwargs, missing in cases: + with pytest.raises(ValueError, match=f"SDK.*{missing}"): + operation(*args, **kwargs) + + +def test_openai_async_resource_groups_validate_unknown_arguments_before_dispatch(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_upload_file, (b"data",), {"purpose": "assistants", "unknown_option": True}), + (provider.async_create_batch, ("file-1",), {"unknown_option": True}), + (provider.async_create_fine_tuning_job, (), {"unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + await operation(*args, **kwargs) + + asyncio.run(run()) + + +def test_openai_async_resource_missing_required_arguments_are_project_errors(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_create_fine_tuning_job, (), {}, "model"), + (provider.async_create_eval, (), {}, "data_source_config"), + (provider.async_create_video, (), {}, "prompt"), + (provider.async_update_conversation, ("conversation-1",), {}, "metadata"), + ) + for operation, args, kwargs, missing in cases: + with pytest.raises(ValueError, match=f"SDK.*{missing}"): + await operation(*args, **kwargs) + + asyncio.run(run()) + + +def test_openai_media_and_management_resources_validate_sdk_arguments(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_container, (), {"name": "demo", "unknown_option": True}), + ( + provider.create_fine_tuning_job, + (), + {"model": "demo", "training_file": "file-1", "unknown_option": True}, + ), + ( + provider.create_eval, + (), + { + "data_source_config": {}, + "testing_criteria": [], + "unknown_option": True, + }, + ), + (provider.generate_image, ("hello",), {"unknown_option": True}), + ( + provider.text_to_speech, + ("hello", "tts-1", "alloy"), + {"unknown_option": True}, + ), + (provider.create_video, (), {"prompt": "hello", "unknown_option": True}), + (provider.create_conversation, (), {"items": [], "unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + operation(*args, **kwargs) + + +def test_openai_responses_extra_body_cannot_override_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"], "extra_body": {"input": "other"}} + + with pytest.raises(ValueError, match="extra_body.*input"): + provider._build_responses_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("field", ["top_k", "repetition_penalty", "reasoning"]) +def test_openai_chat_explicit_extra_body_extensions_cannot_be_silently_overridden(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"extra_body": {field: {"effort": "low"} if field == "reasoning" else 1}} + + kwargs = {field: {"effort": "high"} if field == "reasoning" else 2} + with pytest.raises(ValueError, match=field): + provider._build_chat_request(prompt="hi", stream=False, **kwargs) + + +@pytest.mark.parametrize("field", ["top_k", "seed", "stop", "reasoning"]) +def test_openai_responses_explicit_extra_body_extensions_cannot_be_silently_overridden(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"], "extra_body": {field: 1}} + + kwargs = {field: {"effort": "high"} if field == "reasoning" else 2} + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False, **kwargs) + + +@pytest.mark.parametrize("async_mode", [False, True]) +def test_openai_embedding_extra_body_conflicts_are_explicit(async_mode): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {"extra_body": {"tenant": "configured"}} + provider._client = SimpleNamespace(embeddings=SimpleNamespace(create=lambda **_: None)) + provider._aclient = SimpleNamespace(embeddings=SimpleNamespace(create=lambda **_: None)) + + if async_mode: + async def run(): + await provider.aembed_documents(["text"], extra_body={"tenant": "request"}) + + with pytest.raises(ValueError, match="tenant"): + asyncio.run(run()) + else: + with pytest.raises(ValueError, match="tenant"): + provider.embed_documents(["text"], extra_body={"tenant": "request"}) + + +@pytest.mark.parametrize("async_mode", [False, True]) +@pytest.mark.parametrize( + "field_name, value", + [ + ("extra_headers", {"Authorization": "Bearer secret"}), + ("extra_headers", {"X-API-Key": "secret"}), + ("extra_query", {"access_token": "secret"}), + ("extra_query", {"api_key": "secret"}), + ], +) +def test_openai_embedding_request_overrides_reject_credentials(async_mode, field_name, value): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {} + provider._client = SimpleNamespace( + embeddings=SimpleNamespace(create=lambda **_: SimpleNamespace(data=[])) + ) + + kwargs = {field_name: value} + if async_mode: + async def run(): + await provider.aembed_documents(["text"], **kwargs) + + with pytest.raises(ValueError, match="请求头/query|凭证"): + asyncio.run(run()) + else: + with pytest.raises(ValueError, match="请求头/query|凭证"): + provider.embed_documents(["text"], **kwargs) + + +def test_openai_embedding_rejects_unknown_model_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._options = {"dimensons": 512} + + with pytest.raises(ValueError, match="不支持请求参数: dimensons"): + provider._embedding_options() + + +def test_openai_chat_rejects_unimplemented_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider._build_chat_request(prompt="hi", background=True) + + +def test_non_openai_compatible_provider_requires_explicit_base_url(monkeypatch): + settings = SimpleNamespace(qwen_api_key="token", qwen_base_url=None) + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: settings) + + with pytest.raises(ValueError, match="qwen_base_url"): + OpenAICompatibleProvider("demo", "qwen") + + +def test_non_openai_chat_rejects_openai_only_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="modalities"): + provider._build_chat_request(prompt="hi", modalities=["audio"], stream=False) + + +def test_google_rejects_unimplemented_request_fields(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider.complete(CompletionRequest(prompt="hi", background=True)) + + +def test_openai_complete_preserves_chat_text_tool_calls_and_usage(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace( + choices=[SimpleNamespace( + message=SimpleNamespace( + content="answer", + tool_calls=[SimpleNamespace(id="call-1", type="function", function=SimpleNamespace(name="lookup", arguments='{"id":1}'))], + ), + finish_reason="tool_calls", + )], + usage=SimpleNamespace(prompt_tokens=3, completion_tokens=4, total_tokens=7), + ) + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **_: response))) + result = provider.complete(CompletionRequest(prompt="hi", stream=False)) + assert result.text == "answer" + assert result.tool_calls[0]["name"] == "lookup" + assert result.usage["total_tokens"] == 7 + + +def test_openai_async_complete_preserves_responses_metadata(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = {} + + class Responses: + async def create(self, **_kwargs): + return SimpleNamespace( + id="resp-async", + status="completed", + output_text="异步答复", + usage=SimpleNamespace(input_tokens=4, output_tokens=5, total_tokens=9), + ) + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + result = asyncio.run(provider.acomplete(CompletionRequest(prompt="hi"))) + + assert result.text == "异步答复" + assert result.finish_reason == "completed" + assert result.usage["input_tokens"] == 4 + + +def test_openai_provider_responses_dispatch_uses_inherited_responses_adapter(): + provider = object.__new__(OpenAIProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://api.openai.com/v1" + provider._options = {} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.text == "答复" + assert calls == [ + { + "model": "demo", + "input": "hello", + "stream": False, + "instructions": "You are a helpful assistant.", + } + ] + + +def test_openai_responses_verbosity_uses_text_config(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", stream=False, verbosity="low" + ) + + assert request["text"] == {"verbosity": "low"} + assert "verbosity" not in request + + +def test_openai_responses_parse_does_not_forward_internal_stream_flag(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class Responses: + def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls[0]["text_format"] is dict + assert "stream" not in calls[0] + + +def test_openai_async_responses_parse_does_not_forward_internal_stream_flag(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class Responses: + async def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + assert asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) == "parsed" + assert calls[0]["text_format"] is dict + assert "stream" not in calls[0] + + +@pytest.mark.parametrize("async_mode", [False, True]) +def test_openai_responses_parse_rejects_background_mode(async_mode): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {"background": True} + calls = [] + + class Responses: + def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + class AsyncResponses: + async def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + with pytest.raises(ValueError, match="background"): + if async_mode: + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) + else: + provider.parse(dict, CompletionRequest(prompt="hi")) + + assert calls == [] + + +def test_openai_responses_normalizes_model_options_to_sdk_shape(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "max_tokens": 256, + "verbosity": "low", + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["max_output_tokens"] == 256 + assert request["text"] == { + "format": {"type": "json_object"}, + "verbosity": "low", + } + assert "max_tokens" not in request + assert "response_format" not in request + assert "verbosity" not in request + + +def test_openai_responses_normalizes_completion_and_reasoning_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = { + "max_completion_tokens": 256, + "reasoning_effort": "medium", + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["max_output_tokens"] == 256 + assert request["reasoning"] == {"effort": "medium"} + assert "max_completion_tokens" not in request + assert "reasoning_effort" not in request + + +@pytest.mark.parametrize("field", ["stop", "seed", "top_k"]) +def test_openai_responses_rejects_chat_only_sampling_fields(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False, **{field: 1}) + + +def test_compatible_responses_keeps_vendor_sampling_extensions_in_extra_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", stream=False, stop=["END"], seed=7, top_k=20 + ) + + assert request["extra_body"] == {"stop": ["END"], "seed": 7, "top_k": 20} + + +def test_compatible_responses_allows_supported_openai_response_extensions(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._base_url = "https://gateway.invalid/v1" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", + stream=False, + store=True, + prompt_cache_key="cache-key", + prompt_cache_options={"scope": "user"}, + prompt_cache_retention="24h", + safety_identifier="user-1", + moderation={"mode": "strict"}, + verbosity="low", + ) + + assert request["store"] is True + assert request["prompt_cache_key"] == "cache-key" + assert request["prompt_cache_options"] == {"scope": "user"} + assert request["prompt_cache_retention"] == "24h" + assert request["safety_identifier"] == "user-1" + assert request["moderation"] == {"mode": "strict"} + assert request["text"]["verbosity"] == "low" + + +def test_factory_protocol_status_uses_runtime_verified_options(): + assert ModelProviderFactory.protocol_status( + "qwen", + "responses", + options={"server_verified_protocols": ["responses"]}, + ) == { + "adapter_supported": True, + "server_verified": True, + } + + report = ModelProviderFactory.capability_report( + "qwen", + options={"server_verified_protocols": ["responses"]}, + ) + assert report["server_verified_protocols"] == ["responses"] + assert report["protocol_status"]["responses"]["server_verified"] is True + + +@pytest.mark.parametrize( + ("provider", "alias", "canonical"), + [ + ("openai", "responses", "responses"), + ("openai", "response", "responses"), + ("anthropic", "anthropic", "messages"), + ("anthropic", "anthropic_messages", "messages"), + ("google", "gemini", "generate_content"), + ("google", "generate", "generate_content"), + ], +) +def test_factory_protocol_status_normalizes_protocol_aliases(provider, alias, canonical): + assert ModelProviderFactory.protocol_status(provider, alias) == ModelProviderFactory.protocol_status( + provider, canonical + ) + + +def test_factory_google_protocol_status_accepts_safe_http_options(): + status = ModelProviderFactory.protocol_status( + "google", + "generate_content", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + + assert status == {"adapter_supported": True, "server_verified": False} + report = ModelProviderFactory.capability_report( + "google", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + assert report["protocol_status"]["generate_content"]["server_verified"] is False + + +def test_factory_google_protocol_status_rejects_credentials_in_http_options(): + with pytest.raises(ValueError, match="http_options"): + ModelProviderFactory.protocol_status( + "google", + "generate_content", + options={"http_options": {"headers": {"Authorization": "Bearer secret"}}}, + ) + + +def test_openai_responses_request_values_override_model_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "max_tokens": 256, + "verbosity": "low", + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request( + prompt="hi", + stream=False, + max_tokens=512, + verbosity="high", + response_format={ + "type": "json_schema", + "json_schema": { + "name": "answer", + "schema": {"type": "object"}, + }, + }, + ) + + assert request["max_output_tokens"] == 512 + assert request["text"]["verbosity"] == "high" + assert request["text"]["format"]["name"] == "answer" + + +@pytest.mark.parametrize("field", ["thinking", "caching", "session", "expire_at"]) +def test_openai_responses_rejects_unsupported_model_options(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {field: {"enabled": True} if field != "expire_at" else 123} + + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("protocol", ["chat_completions", "responses"]) +def test_ark_rejects_unknown_model_options(protocol): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = protocol + provider._options = {"unknown_option": True} + + with pytest.raises(ValueError, match="unknown_option"): + if protocol == "responses": + provider._build_responses_request(CompletionRequest(prompt="hi", stream=False)) + else: + provider._build_chat_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("protocol", ["chat_completions", "responses"]) +def test_openai_compatible_rejects_unknown_model_options(protocol): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = protocol + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {"unknown_option": True} + + with pytest.raises(ValueError, match="unknown_option"): + if protocol == "responses": + provider._build_responses_request(prompt="hi", stream=False) + else: + provider._build_chat_request(prompt="hi", stream=False) + + +def test_ark_responses_rejects_top_logprobs_at_request_and_model_boundaries(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + + provider._options = {} + with pytest.raises(ValueError, match="top_logprobs"): + provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False, top_logprobs=2) + ) + + provider._options = {"top_logprobs": 2} + with pytest.raises(ValueError, match="top_logprobs"): + provider._build_responses_request(CompletionRequest(prompt="hi", stream=False)) + + +def test_ark_responses_validates_session_mapping_and_conversation_conflicts(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {} + + with pytest.raises(ValueError, match="conversation.*对象"): + provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False, conversation="conversation-id") + ) + + with pytest.raises(ValueError, match="conversation.*session"): + provider._build_responses_request( + CompletionRequest( + prompt="hi", + stream=False, + conversation={"id": "conversation-id"}, + session={"id": "session-id"}, + ) + ) + + request = provider._build_responses_request( + CompletionRequest( + prompt="hi", stream=False, conversation={"id": "conversation-id"} + ) + ) + assert request["session"] == {"id": "conversation-id"} + + +def test_ark_native_response_resource_requires_explicit_model_and_rejects_unknown_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "configured-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return "response" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(ValueError, match="model"): + provider.resources.create_response(input="raw") + with pytest.raises(ValueError, match="unknown_option"): + provider.resources.create_response( + input="raw", model="caller-model", unknown_option=True + ) + + assert provider.resources.create_response(input="raw", model="caller-model") == "response" + assert calls == [{"input": "raw", "model": "caller-model"}] + + +def test_secret_free_validation_isolates_nested_values(): + options = {"extra_body": {"routing": {"tenant": "demo"}}} + validated_options = validate_secret_free_options(options, "Provider") + validated_payload = validate_secret_free_payload(options, "Provider", "extra_body") + + assert validated_options is not options + assert validated_options["extra_body"] is not options["extra_body"] + assert validated_payload["extra_body"] is not options["extra_body"] + + options["extra_body"]["routing"]["tenant"] = "changed" + assert validated_options["extra_body"]["routing"]["tenant"] == "demo" + assert validated_payload["extra_body"]["routing"]["tenant"] == "demo" + + +def test_resource_security_validation_resolves_extension_key_variants(): + values = { + "Extra-Headers": {"X-Trace": "trace"}, + "EXTRA_QUERY": {"tenant": "demo"}, + "extra-body": {"routing": {"tenant": "demo"}}, + } + validated = validate_secret_free_resource_kwargs(values, "Provider") + + assert validated["Extra-Headers"] == {"X-Trace": "trace"} + assert validated["EXTRA_QUERY"] == {"tenant": "demo"} + assert validated["extra-body"] == {"routing": {"tenant": "demo"}} + assert validated["Extra-Headers"] is not values["Extra-Headers"] + assert validated["extra-body"] is not values["extra-body"] + + +@pytest.mark.parametrize( + "key", + [ + "openai_api_key", + "api-token", + "ApiToken", + "token", + "myClientSecret", + "myApiKey", + "myAPIKey", + "private_key", + "ssh_key", + "ssh-private-key", + "signing_key", + "encryption_key", + ], +) +def test_security_recognizes_separated_credential_key_variants(key): + assert is_sensitive_option_key(key) is True + + +def test_security_allows_non_credential_header_descriptors(): + assert is_sensitive_option_key("X-Authorization-Count") is False + assert is_sensitive_option_key("X-Credential-Provider") is False + + +def test_security_scans_string_values_for_embedded_credentials(): + found = find_sensitive_option_paths( + {"X-Trace": "Bearer secret-token", "tenant": "safe"} + ) + assert "X-Trace" in found + + +@pytest.mark.parametrize( + "value", + [ + '{"api_key": "secret-value"}', + "{'token': 'secret-value'}", + ], +) +def test_security_redacts_json_embedded_credentials(value): + redacted = redact_sensitive_text(value) + assert "secret-value" not in redacted + assert "REDACTED" in redacted + assert find_sensitive_option_paths({"message": value}) == ["message"] + + +@pytest.mark.parametrize( + "value", + [ + "token usage", + "token verification failed", + "what is the token budget", + "token bucket algorithm", + "renew authorization flow", + ], +) +def test_security_does_not_treat_ordinary_text_as_credentials(value): + assert redact_sensitive_text(value) == value + assert find_sensitive_option_paths({"note": value}) == [] + assert validate_secret_free_options({"note": value}, "Provider")["note"] == value + + +def test_security_still_redacts_whitespace_separated_credential_like_values(): + value = "token 0123456789abcdef" + assert "REDACTED" in redact_sensitive_text(value) + assert find_sensitive_option_paths({"value": value}) == ["value"] + + +def test_security_redacts_credentials_in_non_string_exception_values(): + class CredentialError: + def __str__(self): + return "Authorization: Bearer secret-token-123456" + + assert "REDACTED" in redact_sensitive_text(CredentialError()) + + +def test_secret_free_validation_deep_copies_sdk_models(): + http_options = types.HttpOptions(headers={"X-Trace-ID": "trace"}) + validated = validate_secret_free_options( + {"http_options": http_options}, + "Google", + allowed_containers={"http_options", "headers"}, + ) + + http_options.headers["X-Trace-ID"] = "changed" + assert validated["http_options"].headers["X-Trace-ID"] == "trace" + assert validated["http_options"] is not http_options + + +def test_openai_responses_merges_native_text_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "text": {"verbosity": "low"}, + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["text"] == { + "format": {"type": "json_object"}, + "verbosity": "low", + } + + +def test_openai_chat_model_max_tokens_uses_current_sdk_field(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._options = {"max_tokens": 256} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=512) + + assert request["max_completion_tokens"] == 512 + assert "max_tokens" not in request + + +def test_openai_custom_gateway_keeps_legacy_chat_max_tokens_field(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._base_url = "https://gateway.example/v1" + provider._options = {} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=512) + + assert request["max_tokens"] == 512 + assert "max_completion_tokens" not in request + + +@pytest.mark.parametrize("provider_name", ["qwen", "ollama", "lm-studio", "grok", "siliconflow"]) +def test_compatible_chat_models_keep_max_tokens_field(provider_name): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = provider_name + provider._protocol = "chat_completions" + provider._options = {} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=256) + + assert request["max_tokens"] == 256 + assert "max_completion_tokens" not in request + + +def test_google_async_invoke_stream_and_non_stream_issue_one_request_each(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + calls = {"stream": 0, "complete": 0} + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + calls["stream"] += 1 + return iter([ + SimpleNamespace(text="流"), + SimpleNamespace(candidates=[SimpleNamespace(finish_reason="STOP")]), + ]) + + async def generate_content(self, **_kwargs): + calls["complete"] += 1 + return SimpleNamespace(text="非流") + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + async def collect(stream): + return [chunk async for chunk in provider.ainvoke( + prompt="hi", stream=stream, temperature=None + )] + + assert asyncio.run(collect(True)) == ["流"] + assert calls == {"stream": 1, "complete": 0} + assert asyncio.run(collect(False)) == ["非流"] + assert calls == {"stream": 1, "complete": 1} + + +def test_google_query_embedding_uses_query_task(monkeypatch): + provider = object.__new__(GoogleProvider) + provider._model_name = "embedding" + provider._options = {} + response = SimpleNamespace(embeddings=[SimpleNamespace(values=[1.0, 2.0])]) + client = SimpleNamespace(models=SimpleNamespace(embed_content=lambda **kwargs: response)) + provider._get_client = lambda: client + assert provider.embed_documents(["question"], task_type="RETRIEVAL_QUERY") == [[1.0, 2.0]] + + +def test_google_http_extensions_map_to_sdk_http_options(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + config = provider._build_generation_config( + "system", None, 0.2, + extra_headers={"X-Trace": "trace-1"}, + extra_body={"tenant": "demo"}, + timeout=1.25, + ) + + assert config.http_options.headers == {"X-Trace": "trace-1"} + assert config.http_options.extra_body == {"tenant": "demo"} + assert config.http_options.timeout == 1250 + with pytest.raises(ValueError, match="extra_query"): + provider._build_generation_config("system", None, 0.2, extra_query={"tenant": "demo"}) + + +@pytest.mark.parametrize("field", ["timeout", "extra_body"]) +def test_google_http_options_conflict_with_model_level_http_fields_at_init(field): + options = {"http_options": {"headers": {"X-Trace": "trace"}}} + options[field] = 2 if field == "timeout" else {"tenant": "model"} + + with pytest.raises(ValueError, match="http_options.*模型级.*" + field): + GoogleProvider("gemini", options=options) + + +def test_google_model_http_options_apply_to_embedding_and_token_count(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {"timeout": 2, "extra_body": {"tenant": "model"}} + seen = {} + + class Models: + def count_tokens(self, **kwargs): + seen["count"] = kwargs + return SimpleNamespace(total_tokens=3) + + def embed_content(self, **kwargs): + seen["embed"] = kwargs + return SimpleNamespace(embeddings=[SimpleNamespace(values=[1.0, 2.0])]) + + provider._get_client = lambda: SimpleNamespace(models=Models()) + + assert provider.count_tokens(CompletionRequest(prompt="hi")) == 3 + assert provider.embed_documents(["doc"]) == [[1.0, 2.0]] + for call in (seen["count"], seen["embed"]): + assert call["config"].http_options.timeout == 2000 + assert call["config"].http_options.extra_body == {"tenant": "model"} + + +def test_google_request_headers_reject_sensitive_keys(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_generation_config( + "system", + None, + 0.2, + extra_headers={"X-API-Key": "secret"}, + ) + + +def test_google_async_token_count_and_embedding_forward_http_options(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + seen = {} + + class AsyncModels: + async def count_tokens(self, **kwargs): + seen["count"] = kwargs + return SimpleNamespace(total_tokens=7) + + async def embed_content(self, **kwargs): + seen["embed"] = kwargs + return SimpleNamespace(embeddings=[SimpleNamespace(values=[0.1, 0.2])]) + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + request = CompletionRequest( + prompt="hi", extra_headers={"X-Trace": "trace-2"}, + extra_body={"tenant": "demo"}, timeout=2, + ) + + assert asyncio.run(provider.async_count_tokens(request)) == 7 + count_http = seen["count"]["config"].http_options + assert count_http.headers == {"X-Trace": "trace-2"} + assert count_http.extra_body == {"tenant": "demo"} + assert count_http.timeout == 2000 + + assert asyncio.run(provider.aembed_documents( + ["doc"], extra_headers={"X-Trace": "trace-3"}, timeout=3, + )) == [[0.1, 0.2]] + embed_config = seen["embed"]["config"] + assert embed_config.http_options.headers == {"X-Trace": "trace-3"} + assert embed_config.http_options.timeout == 3000 + + +def test_google_compute_tokens_forwards_http_options_sync_and_async(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + seen = {} + + class Models: + def compute_tokens(self, **kwargs): + seen["sync"] = kwargs + return "sync" + + class AsyncModels: + async def compute_tokens(self, **kwargs): + seen["async"] = kwargs + return "async" + + provider._get_client = lambda: SimpleNamespace( + models=Models(), aio=SimpleNamespace(models=AsyncModels()) + ) + request = CompletionRequest( + prompt="hi", + extra_headers={"X-Trace": "trace"}, + extra_body={"tenant": "demo"}, + timeout=2, + ) + + assert provider.compute_tokens(request) == "sync" + assert asyncio.run(provider.async_compute_tokens(request)) == "async" + for call in (seen["sync"], seen["async"]): + assert call["model"] == "gemini" + assert call["config"].http_options.headers == {"X-Trace": "trace"} + assert call["config"].http_options.extra_body == {"tenant": "demo"} + assert call["config"].http_options.timeout == 2000 + + +def test_google_token_count_rejects_ignored_generation_fields(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + with pytest.raises(ValueError, match="max_tokens"): + provider.count_tokens(CompletionRequest(prompt="hi", max_tokens=10)) + with pytest.raises(ValueError, match="tools"): + provider.compute_tokens(CompletionRequest(prompt="hi", tools=[{"type": "function"}])) + + +def test_google_compute_tokens_rejects_gemini_developer_mode(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + provider._get_client = lambda: SimpleNamespace( + _api_client=SimpleNamespace(vertexai=False), + models=SimpleNamespace(compute_tokens=lambda **_: None), + ) + with pytest.raises(ValueError, match="仅支持 Vertex"): + provider.compute_tokens(CompletionRequest(prompt="hi")) + + +def test_openai_embedding_rejects_call_level_model_and_input_override(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {} + provider._client = None + + with pytest.raises(ValueError, match="不允许覆盖请求字段"): + provider.embed_documents(["text"], model="other") + + +def test_google_stream_events_cover_text_thought_tool_usage_and_finish(): + chunk = SimpleNamespace( + response_id="gemini-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace( + parts=[ + SimpleNamespace(text="思考", thought=True), + SimpleNamespace( + function_call=SimpleNamespace(name="lookup", args={"id": 1}) + ), + SimpleNamespace(text="答案", thought=False), + ] + ), + finish_reason="STOP", + ) + ], + usage_metadata=SimpleNamespace(prompt_token_count=2, candidates_token_count=3, total_token_count=5), + ) + + events = GoogleProvider._stream_events_from_chunk(chunk) + + assert [event.type for event in events] == [ + "reasoning_delta", "tool_call_delta", "text_delta", "finish", "usage" + ] + assert events[1].tool_call["arguments"] == {"id": 1} + assert events[-1].usage["total_tokens"] == 5 + + +def test_google_tool_call_merge_preserves_nested_partial_arguments(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "arguments": {"query": {"language": "zh"}}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "arguments": {"query": {"limit": 5}}}, + ) + + assert merged["arguments"] == { + "query": {"language": "zh", "limit": 5}, + } + + +def test_google_tool_call_merge_keeps_index_when_later_fragment_omits_id(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "arguments": {"second": 2}}, + ) + + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_keeps_id_when_later_fragment_has_no_identity_fields(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"first": 1}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"second": 2}}, + ) + + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_migrates_index_to_late_id(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["index"] == 0 + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_keeps_index_when_late_id_also_has_index(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "index": 0, "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert list(accumulator) == [("index", 0)] + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_prefers_index_when_later_fragment_has_index(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["index"] == 0 + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_rejects_ambiguous_unidentified_parallel_calls(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"first": 1}}, + ) + + with pytest.raises(RuntimeError, match="缺少稳定 id/index"): + GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"first": 2}}, + ) + + +def test_google_sync_stream_merges_tool_call_when_id_arrives_after_first_chunk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"first": 1})), + chunk(SimpleNamespace(id="call-1", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + assert completed[0].response_id == "response-1" + assert completed[0].raw.response_id == "response-1" + assert completed[0].tool_call == { + "type": "function", + "arguments": {"first": 1, "second": 2}, + "name": "lookup", + "id": "call-1", + } + + +def test_google_sync_stream_defers_unidentified_tool_completion_until_finish(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"id": 1}, will_continue=False)), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + + +def test_google_sync_stream_does_not_duplicate_string_tool_arguments_on_completion(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + function_call = SimpleNamespace( + id="call-1", + name="lookup", + args='{"id":1}', + will_continue=False, + ) + chunk = SimpleNamespace( + response_id="response-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]), + finish_reason="STOP", + ) + ], + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: iter([chunk])) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert completed[0].tool_call["arguments"] == '{"id":1}' + + +def test_google_async_stream_merges_tool_call_when_id_arrives_after_first_chunk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + async def chunks(): + yield chunk(SimpleNamespace(name="lookup", args={"first": 1})) + yield chunk(SimpleNamespace(id="call-1", args={"second": 2})) + yield chunk(finish_reason="STOP") + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + assert completed[0].response_id == "response-1" + assert completed[0].raw is not None + assert completed[0].tool_call == { + "type": "function", + "arguments": {"first": 1, "second": 2}, + "name": "lookup", + "id": "call-1", + } + + +def test_google_async_stream_does_not_duplicate_string_tool_arguments_on_completion(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + function_call = SimpleNamespace( + id="call-1", + name="lookup", + args='{"id":1}', + will_continue=False, + ) + chunk = SimpleNamespace( + response_id="response-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]), + finish_reason="STOP", + ) + ], + ) + + async def chunks(): + yield chunk + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert completed[0].tool_call["arguments"] == '{"id":1}' + + +def test_google_sync_stream_keeps_parallel_tool_calls_separate_without_index(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(id="call-1", name="lookup", args={"first": 1})), + chunk(SimpleNamespace(id="call-2", name="search", args={"first": 2})), + chunk(SimpleNamespace(id="call-1", args={"second": 1})), + chunk(SimpleNamespace(id="call-2", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert [event.type for event in events][-3:] == [ + "tool_call_completed", + "tool_call_completed", + "finish", + ] + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_async_stream_keeps_parallel_tool_calls_separate_without_index(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + async def chunks(): + yield chunk(SimpleNamespace(id="call-1", name="lookup", args={"first": 1})) + yield chunk(SimpleNamespace(id="call-2", name="search", args={"first": 2})) + yield chunk(SimpleNamespace(id="call-1", args={"second": 1})) + yield chunk(SimpleNamespace(id="call-2", args={"second": 2})) + yield chunk(finish_reason="STOP") + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert [event.type for event in events][-3:] == [ + "tool_call_completed", + "tool_call_completed", + "finish", + ] + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_sync_stream_migrates_parallel_unidentified_calls_by_name(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"first": 1})), + chunk(SimpleNamespace(name="search", args={"first": 2})), + chunk(SimpleNamespace(id="call-1", name="lookup", args={"second": 1})), + chunk(SimpleNamespace(id="call-2", name="search", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["first"] for event in completed} == {1, 2} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_contents_links_function_call_content_to_followup_tool_result(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "content": [ + { + "type": "function_call", + "id": "call-1", + "name": "lookup", + "arguments": {"id": 1}, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"value": 2}', + }, + ], + None, + None, + ) + + function_call = contents[0].parts[0].function_call + function_response = contents[1].parts[0].function_response + assert function_call.id == "call-1" + assert function_call.name == "lookup" + assert function_response.id == "call-1" + assert function_response.name == "lookup" + assert function_response.response == {"value": 2} + + +@pytest.mark.parametrize("value", ["123abc", "-", "01", "-01"]) +def test_google_tool_result_numeric_prefix_that_is_not_json_stays_text(value): + contents = GoogleProvider._contents( + [ + {"role": "assistant", "content": [{"type": "function_call", "id": "call-1", "name": "lookup", "arguments": {}}]}, + {"role": "tool", "tool_call_id": "call-1", "content": value}, + ], + None, + None, + ) + + assert contents[1].parts[0].function_response.response == {"output": value} + + +@pytest.mark.parametrize("value", ["0", "-1", "1.25", "2e3"]) +def test_google_tool_result_complete_json_number_is_decoded(value): + contents = GoogleProvider._contents( + [ + {"role": "assistant", "content": [{"type": "function_call", "id": "call-1", "name": "lookup", "arguments": {}}]}, + {"role": "tool", "tool_call_id": "call-1", "content": value}, + ], + None, + None, + ) + + assert contents[1].parts[0].function_response.response == {"output": json.loads(value)} + + +def test_anthropic_stream_events_cover_text_thinking_tool_usage_and_finish(): + events = [ + AnthropicProvider._stream_event( + SimpleNamespace(type="content_block_delta", delta=SimpleNamespace(type="text_delta", text="答")), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace(type="content_block_delta", delta=SimpleNamespace(type="thinking_delta", thinking="想")), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace( + type="content_block_delta", + index=0, + delta=SimpleNamespace(type="input_json_delta", partial_json='{"id":1}'), + ), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace( + type="message_delta", + delta=SimpleNamespace(stop_reason="tool_use"), + usage=SimpleNamespace(input_tokens=2, output_tokens=3), + ), + "msg-1", + ), + ] + + assert [event.type for event in events if event] == [ + "text_delta", "reasoning_delta", "tool_call_delta", "finish" + ] + assert events[3].finish_reason == "tool_use" + assert events[3].usage["output_tokens"] == 3 + + +def test_ark_stream_events_cover_chat_and_responses_terminal_metadata(): + chat = VolcengineProvider._chat_stream_event( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content="想", tool_calls=None), + finish_reason=None, + ) + ], + ) + ) + provider = object.__new__(VolcengineProvider) + response = provider._responses_stream_event( + SimpleNamespace( + type="response.completed", + response=SimpleNamespace( + id="ark-1", status="completed", usage=SimpleNamespace(input_tokens=1, output_tokens=2) + ), + ) + ) + + assert chat.type == "text_delta" + assert chat.text == "答" + assert response.type == "finish" + assert response.finish_reason == "completed" + assert ( + response.usage["total_tokens"] + if "total_tokens" in response.usage + else response.usage["input_tokens"] + ) == 1 + + +def test_ark_invoke_non_stream_rejects_failed_response_status(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_kwargs): + return SimpleNamespace( + status="failed", error=SimpleNamespace(message="ark failed") + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="ark failed"): + list(provider.invoke("hi", stream=False)) + + +def test_ark_ainvoke_non_stream_rejects_failed_response_status(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + async def create(self, **_kwargs): + return SimpleNamespace( + status="failed", error=SimpleNamespace(message="ark async failed") + ) + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [item async for item in provider.ainvoke("hi", stream=False)] + + with pytest.raises(RuntimeError, match="ark async failed"): + asyncio.run(collect()) + + +def test_ark_non_stream_result_preserves_chat_reasoning_content(): + response = SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace( + content="答复", reasoning_content="思考过程", tool_calls=[] + ), + finish_reason="stop", + ) + ], + usage=SimpleNamespace(input_tokens=1, output_tokens=2), + ) + result = VolcengineProvider._extract_result(response) + + assert result.text == "答复" + assert result.reasoning == "思考过程" + + +def test_ark_responses_result_does_not_replace_answer_with_reasoning_text(): + response = SimpleNamespace( + output_text="最终答案", + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="思考过程")], + ) + ], + choices=[], + usage=None, + status="completed", + id="resp-1", + ) + + result = VolcengineProvider._extract_result(response) + + assert result.text == "最终答案" + assert result.reasoning == "思考过程" + + +def test_ark_responses_error_and_cancelled_events_are_explicit(): + provider = object.__new__(VolcengineProvider) + + with pytest.raises(RuntimeError, match="rate limit"): + provider._responses_stream_event( + SimpleNamespace(type="response.error", error=SimpleNamespace(message="rate limit")) + ) + + with pytest.raises(RuntimeError, match="取消|cancelled"): + provider._responses_stream_event( + SimpleNamespace(type="response.cancelled", response=SimpleNamespace(status="cancelled")) + ) + + +def test_ark_responses_custom_tool_stream_uses_output_item_metadata(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo hello", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[1].tool_call["type"] == "custom_tool_call" + assert events[1].tool_call["name"] == "run_command" + + +def test_anthropic_rejects_seed_in_complete_request(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + with pytest.raises(ValueError, match="seed"): + provider.complete(CompletionRequest(prompt="hi", seed=7)) + with pytest.raises(ValueError, match="seed"): + provider.parse(dict, CompletionRequest(prompt="hi", seed=7)) + with pytest.raises(ValueError, match="seed"): + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi", seed=7))) + + +def test_sdk_provider_close_and_aclose_release_clients(): + class SyncClient: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + class AsyncClient: + def __init__(self): + self.closed = False + + async def close(self): + self.closed = True + + providers = [ + OpenAICompatibleProvider.__new__(OpenAICompatibleProvider), + VolcengineProvider.__new__(VolcengineProvider), + AnthropicProvider.__new__(AnthropicProvider), + ] + for provider in providers: + sync = SyncClient() + async_client = AsyncClient() + provider._client = sync + provider._aclient = async_client + provider.close() + assert sync.closed is True + assert provider._client is None + assert async_client.closed is True + assert provider._aclient is None + asyncio.run(provider.aclose()) + assert async_client.closed is True + assert provider._aclient is None + + +def test_anthropic_sync_close_requires_aclose_inside_running_event_loop(): + class AsyncClient: + def __init__(self): + self.closed = False + + async def close(self): + self.closed = True + + provider = AnthropicProvider.__new__(AnthropicProvider) + async_client = AsyncClient() + provider._client = None + provider._aclient = async_client + + async def run() -> None: + with pytest.raises(RuntimeError, match="需要异步关闭.*aclose"): + provider.close() + assert provider._aclient is async_client + await provider.aclose() + + asyncio.run(run()) + assert async_client.closed is True + assert provider._aclient is None + + +def test_sdk_provider_aclose_closes_shared_client_once(): + class SharedClient: + def __init__(self): + self.close_calls = 0 + + async def close(self): + self.close_calls += 1 + + for provider_type in ( + OpenAICompatibleProvider, + VolcengineProvider, + AnthropicProvider, + ): + provider = provider_type.__new__(provider_type) + shared = SharedClient() + provider._client = shared + provider._aclient = shared + + asyncio.run(provider.aclose()) + + assert shared.close_calls == 1 + assert provider._client is None + assert provider._aclient is None + + +def test_google_close_releases_sync_client_and_async_close_releases_aio_client(): + class SyncClient: + def __init__(self): + self.closed = False + self.aio = self + + def close(self): + self.closed = True + + async def aclose(self): + self.closed = True + + provider = GoogleProvider.__new__(GoogleProvider) + client = SyncClient() + provider._client = client + + provider.close() + assert client.closed is True + assert provider._client is None + + provider._client = client + asyncio.run(provider.aclose()) + assert client.closed is True + assert provider._client is None + + +def test_google_vertex_gemini_embedding_uses_single_content_path(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-embedding-001" + + assert provider._vertex_embed_content_only() is True + + +def test_anthropic_complete_extracts_tool_blocks_and_usage(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + response = SimpleNamespace( + content=[ + SimpleNamespace(type="text", text="answer"), + SimpleNamespace(type="tool_use", id="call-1", name="lookup", input={"id": 1}), + ], + usage=SimpleNamespace(input_tokens=4, output_tokens=5), + stop_reason="tool_use", + ) + provider._get_client = lambda: SimpleNamespace(messages=SimpleNamespace(create=lambda **_: response)) + result = provider.complete(CompletionRequest(prompt="hi", stream=False)) + assert result.text == "answer" + assert result.tool_calls[0]["arguments"] == {"id": 1} + assert result.usage == {"input_tokens": 4, "output_tokens": 5} + + +def test_anthropic_user_maps_to_sdk_user_profile_header(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + params = provider._build_message_params("hi", user="profile-1") + assert params["user_profile_id"] == "profile-1" + assert "user_id" not in params.get("metadata", {}) + + +def test_openai_parse_and_compact_facades_forward_native_methods(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + calls = {} + + class Completions: + def parse(self, **kwargs): + calls["parse"] = kwargs + return "parsed" + + class Responses: + def compact(self, **kwargs): + calls["compact"] = kwargs + return "compacted" + + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()), responses=Responses() + ) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls["parse"]["model"] == "demo" + assert "stream" not in calls["parse"] + provider._protocol = "responses" + assert provider.compact_responses( + input="hello", previous_response_id="resp-1", timeout=2 + ) == "compacted" + assert calls["compact"] == { + "model": "demo", + "input": "hello", + "previous_response_id": "resp-1", + "timeout": 2, + } + + +def test_openai_compatible_responses_parse_and_compact_require_verified_server_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + provider._options = {} + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.parse(dict, CompletionRequest(prompt="hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + provider.compact_responses(input="hello") + with pytest.raises(ValueError, match="未验证 Responses"): + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) + with pytest.raises(ValueError, match="未验证 Responses"): + asyncio.run(provider.async_compact_responses(input="hello")) + + +@pytest.mark.parametrize("interval", [0, -1, "0"]) +def test_openai_responses_background_poll_interval_must_be_positive(interval): + provider = object.__new__(OpenAICompatibleProvider) + provider._options = {"background_poll_interval": interval} + with pytest.raises(ValueError, match="background_poll_interval.*正数"): + provider._background_poll_config() + + +def test_openai_custom_base_url_requires_verified_responses_resource_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.retrieve_response("resp-1") + + +def test_openai_custom_base_url_blocks_regular_responses_requests(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.complete(CompletionRequest(prompt="hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + list(provider.invoke("hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + list(provider.stream_events(CompletionRequest(prompt="hi"))) + + +def test_openai_custom_base_url_blocks_responses_batch_requests(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.create_batch("file-1") + + +def test_openai_official_base_url_allows_responses_resource_contract(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://api.openai.com/v1/" + provider._server_verified_protocols = frozenset() + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(retrieve=lambda response_id, **kwargs: (response_id, kwargs)) + ) + + assert provider.retrieve_response("resp-1", include=["output"]) == ( + "resp-1", {"include": ["output"]} + ) + + +def test_openai_eval_output_item_retrieve_forwards_sync_and_async_resources(): + provider = object.__new__(OpenAIProvider) + provider._model_name = "demo" + calls = {} + + class OutputItems: + def retrieve(self, output_item_id, **kwargs): + calls["sync"] = (output_item_id, kwargs) + return "retrieved" + + provider._get_client = lambda: SimpleNamespace( + evals=SimpleNamespace(runs=SimpleNamespace(output_items=OutputItems())) + ) + assert provider.retrieve_eval_run_output_item("eval-1", "run-1", "item-1", limit=2) == "retrieved" + assert calls["sync"] == ("item-1", {"eval_id": "eval-1", "run_id": "run-1", "limit": 2}) + assert provider.resources.retrieve_eval_run_output_item("eval-1", "run-1", "item-1") == "retrieved" + + class AsyncOutputItems: + async def retrieve(self, output_item_id, **kwargs): + calls["async"] = (output_item_id, kwargs) + return "async-retrieved" + + provider._get_aclient = lambda: SimpleNamespace( + evals=SimpleNamespace(runs=SimpleNamespace(output_items=AsyncOutputItems())) + ) + assert asyncio.run( + provider.async_retrieve_eval_run_output_item("eval-1", "run-1", "item-1", limit=3) + ) == "async-retrieved" + assert calls["async"] == ("item-1", {"eval_id": "eval-1", "run_id": "run-1", "limit": 3}) + assert asyncio.run( + provider.async_resources.retrieve_eval_run_output_item("eval-1", "run-1", "item-1") + ) == "async-retrieved" + + +def test_resource_facade_exposes_native_sdk_clients(): + provider = object.__new__(OpenAICompatibleProvider) + sync_client = object() + async_client = object() + provider._get_client = lambda: sync_client + provider._get_aclient = lambda: async_client + + assert provider.resources.native is sync_client + assert provider.async_resources.native is sync_client + assert provider.async_resources.async_native is async_client + + +def test_official_resource_facade_keeps_missing_dynamic_attributes_normal(): + provider = object.__new__(OpenAIProvider) + provider._get_client = lambda: SimpleNamespace(existing="value") + provider._get_aclient = lambda: SimpleNamespace(existing="async-value") + + resources = provider.resources + assert resources.existing == "value" + assert not hasattr(resources, "missing_resource") + assert not hasattr(resources, "__missing_resource__") + + +def test_openai_resources_expose_new_official_top_level_resources(): + provider = object.__new__(OpenAIProvider) + sync_resources = SimpleNamespace( + skills="skills", + realtime="realtime", + webhooks="webhooks", + admin="admin", + content_provenance_checks="provenance", + ) + async_resources = SimpleNamespace( + skills="async-skills", + realtime="async-realtime", + webhooks="async-webhooks", + admin="async-admin", + content_provenance_checks="async-provenance", + ) + provider._get_client = lambda: sync_resources + provider._get_aclient = lambda: async_resources + + assert provider.resources.skills == "skills" + assert provider.resources.realtime == "realtime" + assert provider.resources.webhooks == "webhooks" + assert provider.resources.admin == "admin" + assert provider.resources.content_provenance_checks == "provenance" + assert provider.async_resources.skills == "async-skills" + assert provider.async_resources.realtime == "async-realtime" + assert provider.async_resources.webhooks == "async-webhooks" + assert provider.async_resources.admin == "async-admin" + assert provider.async_resources.content_provenance_checks == "async-provenance" + + +def test_openai_capabilities_include_current_top_level_resource_families(): + capabilities = ModelProviderFactory.capabilities("openai") + assert { + "skills", + "realtime", + "webhooks", + "admin", + "content_provenance_checks", + }.issubset(capabilities) + + +def test_factory_capability_report_includes_sdk_and_protocol_evidence(): + report = ModelProviderFactory.capability_report("openai") + + assert report["provider"] == "openai" + assert report["resolved_provider"] == "openai" + assert report["role"] == "llm" + assert report["integration"] == "native_sdk" + assert report["sdk"] == {"package": "openai", "version": report["sdk"]["version"]} + assert report["sdk"]["version"] + assert report["protocol_status"]["responses"] == { + "adapter_supported": True, + "server_verified": False, + } + + +def test_factory_capabilities_are_queryable_by_role_and_alias(): + embedding = ModelProviderFactory.capability_report("qwen", "embedding") + assert embedding["role"] == "embedding" + assert embedding["integration"] == "openai_compatible_sdk" + assert embedding["sdk"]["package"] == "openai" + assert "responses" not in embedding["protocol_status"] + + rerank = ModelProviderFactory.capability_report("siliconflow", "rerank") + assert rerank["resolved_provider"] == "siliconflow_rerank" + assert rerank["integration"] == "http_json" + assert rerank["sdk"]["package"] == "httpx" + assert rerank["protocols"] == [] + + +def test_factory_capability_report_rejects_unknown_role(): + with pytest.raises(ValueError, match="不支持的模型角色"): + ModelProviderFactory.capability_report("openai", "moderation") + + +def test_official_protocol_status_does_not_claim_unverified_custom_endpoints(): + assert ModelProviderFactory.protocol_status("openai", "chat_completions") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("openai", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("volcengine", "ark") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("volcengine", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + + +def test_factory_rejects_server_verified_protocol_alias_not_supported_by_provider(): + with pytest.raises(ValueError, match="不适用于当前提供商"): + ModelProviderFactory.capability_report( + "qwen", options={"server_verified_protocols": ["gemini"]} + ) + with pytest.raises(ValueError, match="不适用于当前提供商"): + ModelProviderFactory.protocol_status( + "qwen", "generate_content", options={"server_verified_protocols": ["gemini"]} + ) + + +def test_google_auth_tokens_facade_forwards_sync_and_async_clients(): + provider = object.__new__(GoogleProvider) + calls = {} + + class Tokens: + def create(self, **kwargs): + calls["sync"] = kwargs + return "token" + + class AsyncTokens: + async def create(self, **kwargs): + calls["async"] = kwargs + return "async-token" + + sync_tokens = Tokens() + async_tokens = AsyncTokens() + provider._get_client = lambda: SimpleNamespace( + auth_tokens=sync_tokens, aio=SimpleNamespace(auth_tokens=async_tokens) + ) + + # Facade 资源经过安全代理;显式 native 出口仍保留 SDK 原生身份。 + assert provider.resources.auth_tokens._value is sync_tokens + assert provider.async_resources.auth_tokens._value is async_tokens + assert provider.resources.native.auth_tokens is sync_tokens + assert provider.async_resources.async_native.auth_tokens is async_tokens + assert provider.resources.create_auth_token(config={}) == "token" + assert asyncio.run(provider.async_resources.create_auth_token(config={})) == "async-token" + assert calls == {"sync": {"config": {}}, "async": {"config": {}}} + + with pytest.raises(ValueError, match="凭证"): + provider.resources.auth_tokens.create(extra_headers={"X-API-Key": "secret"}) + + +def test_google_register_files_facade_allows_auth_business_parameter_sync_and_async(): + provider = object.__new__(GoogleProvider) + auth = AnonymousCredentials() + calls = {} + + class Files: + def register_files(self, **kwargs): + calls["sync"] = kwargs + return "registered" + + class AsyncFiles: + async def register_files(self, **kwargs): + calls["async"] = kwargs + return "async-registered" + + provider._get_client = lambda: SimpleNamespace( + files=Files(), aio=SimpleNamespace(files=AsyncFiles()) + ) + + assert provider.resources.register_files(auth=auth, uris=["gs://bucket/file"]) == "registered" + assert asyncio.run( + provider.async_resources.register_files(auth=auth, uris=["gs://bucket/file"]) + ) == "async-registered" + assert calls == { + "sync": {"auth": auth, "uris": ["gs://bucket/file"]}, + "async": {"auth": auth, "uris": ["gs://bucket/file"]}, + } + + +def test_google_register_files_facade_rejects_non_credentials_auth_before_sdk_call(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: pytest.fail("非法 auth 不应初始化 Google SDK 客户端") + + with pytest.raises(ValueError, match="Credentials"): + provider.resources.register_files( + auth={"api_key": "not-allowed"}, + uris=["gs://bucket/file"], + ) + + +def test_google_register_files_facade_still_rejects_credential_request_overrides(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: pytest.fail("敏感请求覆盖不应初始化 Google SDK 客户端") + + with pytest.raises(ValueError, match="凭证"): + provider.resources.register_files( + auth=AnonymousCredentials(), + uris=["gs://bucket/file"], + extra_headers={"Authorization": "Bearer secret"}, + ) + + +def test_anthropic_beta_resource_facade_forwards_current_sdk_tree(): + provider = object.__new__(AnthropicProvider) + resources = { + name: object() + for name in ( + "agents", "deployments", "deployment_runs", "dreams", "environments", + "files", "memory_stores", "models", "sessions", "skills", "tunnels", + "user_profiles", "vaults", "webhooks", + ) + } + provider._get_client = lambda: SimpleNamespace(beta=SimpleNamespace(**resources)) + provider._get_aclient = lambda: SimpleNamespace(beta=SimpleNamespace(**resources)) + + for name, value in resources.items(): + assert getattr(provider.resources, f"beta_{name}")._value is value + assert getattr(provider.async_resources, f"beta_{name}")._value is value + + +def test_anthropic_direct_resource_calls_reject_sensitive_http_overrides(): + provider = object.__new__(AnthropicProvider) + + class Files: + def list(self, **_kwargs): + raise AssertionError("SDK should not be called") + + class Batches: + def list(self, **_kwargs): + raise AssertionError("SDK should not be called") + + provider._get_client = lambda: SimpleNamespace( + beta=SimpleNamespace(files=Files()), + messages=SimpleNamespace(batches=Batches()), + ) + + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_batches(extra_query={"access_token": "secret"}) + + +def test_google_resource_model_methods_forward_to_sdk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + calls = {} + + class Models: + def delete(self, **kwargs): + calls["delete"] = kwargs + return "deleted" + + def update(self, **kwargs): + calls["update"] = kwargs + return "updated" + + provider._get_client = lambda: SimpleNamespace(models=Models()) + assert provider.resources.delete_model("models/demo") == "deleted" + assert provider.resources.update_model("models/demo", config={"display_name": "demo"}) == "updated" + assert calls["delete"] == {"model": "models/demo"} + assert calls["update"] == {"model": "models/demo", "config": {"display_name": "demo"}} + + +def test_google_operation_and_token_count_facades_match_current_sdk_signature(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + calls = {} + + class Operations: + def get(self, operation, **kwargs): + calls["operation"] = (operation, kwargs) + return "operation" + + class Models: + def count_tokens(self, **kwargs): + calls["count"] = kwargs + return SimpleNamespace(total_tokens=6) + + provider._get_client = lambda: SimpleNamespace(operations=Operations(), models=Models()) + assert provider.resources.get_operation("operations/1", config={}) == "operation" + assert provider.resources.count_tokens(CompletionRequest(prompt="hi")) == 6 + assert calls["operation"] == ("operations/1", {"config": {}}) + assert calls["count"]["model"] == "gemini" + + +def test_google_async_operation_uses_sdk_config_keyword(): + provider = object.__new__(GoogleProvider) + calls = {} + + class Operations: + def get(self, operation, **kwargs): + calls["operation"] = (operation, kwargs) + return "operation" + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(operations=Operations()) + ) + + assert asyncio.run(provider.async_get_operation("operations/2", timeout=2)) == "operation" + operation, kwargs = calls["operation"] + assert operation == "operations/2" + assert kwargs["config"].http_options.timeout == 2000 + assert set(kwargs) == {"config"} + + +def test_google_chat_creation_wraps_generation_options_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = {} + + class Chats: + def create(self, **kwargs): + calls["sync"] = kwargs + return "chat" + + provider._get_client = lambda: SimpleNamespace(chats=Chats()) + + assert provider.create_chat( + history=[], temperature=0.2, max_output_tokens=64, extra_headers={"X-Trace": "1"} + ) == "chat" + assert calls["sync"]["model"] == "gemini-default" + assert calls["sync"]["history"] == [] + assert calls["sync"]["config"].temperature == 0.2 + assert calls["sync"]["config"].max_output_tokens == 64 + assert calls["sync"]["config"].http_options.headers == {"X-Trace": "1"} + assert set(calls["sync"]) == {"model", "history", "config"} + + +def test_google_async_chat_creation_wraps_generation_options_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = {} + + class Chats: + def create(self, **kwargs): + calls["async"] = kwargs + return ("chat", kwargs) + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(chats=Chats()) + ) + + result = asyncio.run( + provider.async_create_chat(history=[], top_p=0.8, response_mime_type="application/json") + ) + assert result[0] == "chat" + assert calls["async"]["config"].top_p == 0.8 + assert calls["async"]["config"].response_mime_type == "application/json" + assert set(calls["async"]) == {"model", "history", "config"} + + +def test_openai_compatible_capabilities_do_not_claim_official_resources(): + assert "files" not in OpenAICompatibleProvider.capabilities + assert "batches" not in OpenAICompatibleProvider.capabilities + assert "vector_stores" not in OpenAICompatibleProvider.capabilities + assert "files" in ModelProviderFactory.capabilities("openai") + assert "files" not in ModelProviderFactory.capabilities("qwen") + + +def test_openai_compatible_resources_only_expose_native_client(): + compatible = object.__new__(OpenAICompatibleProvider) + compatible._get_client = lambda: object() + compatible._get_aclient = lambda: object() + assert hasattr(compatible.resources, "native") + assert not hasattr(compatible.resources, "upload_file") + + official = object.__new__(OpenAIProvider) + official._get_client = lambda: object() + official._get_aclient = lambda: object() + assert hasattr(official.resources, "upload_file") + + +def test_openai_compatible_direct_official_resources_require_declared_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._options = {} + + with pytest.raises(NotImplementedError, match="files"): + provider.list_files() + with pytest.raises(NotImplementedError, match="vector_stores"): + provider.create_vector_store(name="demo") + with pytest.raises(NotImplementedError, match="fine_tuning"): + provider.list_fine_tuning_jobs() + with pytest.raises(NotImplementedError, match="audio"): + provider.text_to_speech("hello", "tts-1", "alloy") + + +def test_google_provider_missing_experimental_resources_is_explicit(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="(?i)agents"): + provider.create_agent(name="demo") + with pytest.raises(NotImplementedError, match="(?i)auth token"): + provider.create_auth_token() + with pytest.raises(AttributeError, match="(?i)agents"): + _missing_agents = provider.resources.agents + + +def test_factory_capabilities_follow_role_specific_provider_contracts(): + assert "embedding" not in ModelProviderFactory.capabilities("deepseek", "llm") + assert ModelProviderFactory.capabilities("qwen", "embedding") == frozenset({"embedding"}) + assert ModelProviderFactory.capabilities("siliconflow", "rerank") == frozenset({"rerank"}) + + +def test_factory_protocol_status_infers_non_llm_role_for_diagnostics(): + assert ModelProviderFactory.protocol_status("local-hash", "responses") == { + "adapter_supported": False, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("siliconflow", "responses", role="rerank") == { + "adapter_supported": False, + "server_verified": False, + } + + +def test_protocol_status_separates_adapter_support_from_server_verification(): + assert ModelProviderFactory.protocol_status("qwen", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + + +@pytest.mark.parametrize("configured", [1, {}, ["responses", 1], ["responses", ""]]) +def test_openai_verified_protocol_configuration_rejects_invalid_types(configured): + provider = object.__new__(OpenAICompatibleProvider) + with pytest.raises(ValueError, match="server_verified_protocols"): + provider._normalize_verified_protocols(configured) + + +def test_openai_responses_token_count_forwards_full_request_extensions(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class TokenCounter: + def count(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(input_tokens=11) + + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(input_tokens=TokenCounter()) + ) + request = CompletionRequest( + prompt="hi", + response_format={"type": "json_object"}, + parallel_tool_calls=True, + reasoning={"effort": "low"}, + truncation="auto", + extra_body={"vendor_flag": True}, + extra_headers={"X-Trace": "trace-1"}, + extra_query={"tenant": "demo"}, + timeout=3, + ) + + assert provider.count_input_tokens(request) == 11 + assert calls == [{ + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + "parallel_tool_calls": True, + "reasoning": {"effort": "low"}, + "text": {"format": {"type": "json_object"}}, + "truncation": "auto", + "extra_headers": {"X-Trace": "trace-1"}, + "extra_query": {"tenant": "demo"}, + "extra_body": {"vendor_flag": True}, + "timeout": 3, + }] + + +def test_openai_responses_token_count_converts_chat_tool_choice(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + + calls = [] + + class TokenCounter: + def count(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(input_tokens=3) + + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(input_tokens=TokenCounter()) + ) + + request = CompletionRequest( + prompt="hi", + tools=[{"type": "function", "function": {"name": "lookup"}}], + tool_choice={"type": "function", "function": {"name": "lookup"}}, + ) + + assert provider.count_input_tokens(request) == 3 + assert calls[0]["tool_choice"] == {"type": "function", "name": "lookup"} + + +def test_openai_responses_token_count_rejects_generation_only_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + with pytest.raises(ValueError, match="max_tokens"): + provider.count_input_tokens(CompletionRequest(prompt="hi", max_tokens=16)) + + +def test_openai_compatible_responses_resources_require_verified_server_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.retrieve_response("resp-1") + + +def test_openai_compatible_verified_responses_resource_is_forwarded(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(retrieve=lambda response_id, **kwargs: (response_id, kwargs)) + ) + + assert provider.retrieve_response("resp-1", include=["output"]) == ( + "resp-1", {"include": ["output"]} + ) + + +def test_anthropic_extra_body_and_request_extensions_keep_sdk_names(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + params = provider._build_message_params( + prompt="hi", + extra_body={"vendor_flag": True}, + extra_headers={"X-Trace": "trace-1"}, + extra_query={"tenant": "demo"}, + ) + + assert params["extra_body"] == {"vendor_flag": True} + assert params["extra_headers"] == {"X-Trace": "trace-1"} + assert params["extra_query"] == {"tenant": "demo"} + assert "vendor_flag" not in params + + +def test_anthropic_request_builder_rejects_sensitive_http_overrides(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_message_params( + prompt="hi", + extra_headers={"Authorization": "Bearer secret"}, + ) + with pytest.raises(ValueError, match="请求头/query"): + provider._build_message_params( + prompt="hi", + extra_query={"access_token": "secret"}, + ) + + +def test_openai_compatible_server_capability_option_is_not_sent_as_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._provider = "qwen" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {"server_verified_protocols": ["responses"], "top_k": 20} + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"top_k": 20} + assert "server_verified_protocols" not in request + + +def test_openai_compatible_responses_model_stop_and_seed_use_extra_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._provider = "qwen" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = { + "server_verified_protocols": ["responses"], + "stop": ["END"], + "seed": 11, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"stop": ["END"], "seed": 11} + assert "stop" not in request + assert "seed" not in request + + +def test_anthropic_client_options_are_not_forwarded_to_message_request(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = { + "base_url": "https://proxy.invalid", + "timeout": 3, + "max_tokens": 8192, + "top_p": 0.8, + } + params = provider._build_message_params(prompt="hi") + assert params["max_tokens"] == 8192 + assert params["extra_body"]["top_p"] == 0.8 + assert "top_p" not in params + assert "base_url" not in params + assert "timeout" not in params + assert provider._build_message_params(prompt="hi", timeout=4)["timeout"] == 4 + + +def test_anthropic_sampling_fields_follow_installed_sdk_request_shape(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + params = provider._build_message_params( + prompt="hi", + temperature=0.2, + top_p=0.8, + top_k=32, + ) + + assert params["extra_body"] == { + "temperature": 0.2, + "top_p": 0.8, + "top_k": 32, + } + assert not {"temperature", "top_p", "top_k"}.intersection(params) + + +def test_anthropic_extra_body_sampling_conflicts_are_explicit(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="temperature.*extra_body"): + provider._build_message_params( + prompt="hi", + temperature=0.2, + extra_body={"temperature": 0.4}, + ) + + +def test_anthropic_beta_tool_runner_rejects_unsupported_compaction_control(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="compaction_control"): + provider.beta_tool_runner([], prompt="hi", compaction_control={"enabled": True}) + + +def test_anthropic_output_format_follows_create_and_stream_sdk_shapes(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="create 不支持 output_format"): + provider._build_message_params(prompt="hi", output_format=dict, stream=False) + + params = provider._build_message_params(prompt="hi", output_format=dict, stream=True) + assert params["output_format"] is dict + + +def test_anthropic_beta_create_rejects_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="create 不支持 output_format"): + provider.beta_create(CompletionRequest(prompt="hi", output_format={"type": "json_schema"})) + + +def test_anthropic_async_beta_create_rejects_output_format_before_sdk_call(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = [] + + class Messages: + async def create(self, **kwargs): + calls.append(kwargs) + return "beta" + + provider._get_aclient = lambda: SimpleNamespace( + beta=SimpleNamespace(messages=Messages()) + ) + + with pytest.raises(ValueError, match="create 不支持 output_format"): + asyncio.run( + provider.async_beta_create( + CompletionRequest(prompt="hi", output_format={"type": "json_schema"}) + ) + ) + assert calls == [] + + +def test_anthropic_beta_token_count_rejects_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="token count 不支持 output_format"): + provider.beta_count_tokens( + CompletionRequest(prompt="hi", output_format={"type": "json_schema"}) + ) + + +def test_anthropic_parse_ignores_configured_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {"output_format": {"type": "json_schema"}} + calls = {} + + class Messages: + def parse(self, **kwargs): + calls.update(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls["output_format"] is dict + assert calls["output_format"] != provider._options["output_format"] + + +def test_anthropic_request_keys_match_installed_sdk_create_signatures(): + import anthropic + + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + params = provider._build_message_params(prompt="hi") + + create_keys = set(inspect.signature(anthropic.Anthropic(api_key="x").messages.create).parameters) + assert set(params).issubset(create_keys) + + beta_params = provider._build_message_params(prompt="hi", beta=True) + beta_keys = set(inspect.signature(anthropic.Anthropic(api_key="x").beta.messages.create).parameters) + assert set(beta_params).issubset(beta_keys) + + +def test_anthropic_modern_models_omit_deprecated_sampling_defaults(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-sonnet-4-6" + provider._options = {} + + params = provider._build_message_params(prompt="hi", temperature=0.7) + + assert "temperature" not in params.get("extra_body", {}) + with pytest.raises(ValueError, match="temperature"): + provider._build_message_params(prompt="hi", temperature=0.2) + with pytest.raises(ValueError, match="top_k"): + provider._build_message_params(prompt="hi", top_k=5) + + +def test_anthropic_modern_models_allow_explicit_legacy_sampling_for_proxies(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-sonnet-4-6" + provider._options = {"allow_deprecated_sampling": True} + + params = provider._build_message_params( + prompt="hi", + temperature=0.2, + top_p=0.8, + top_k=32, + ) + + assert params["extra_body"] == { + "temperature": 0.2, + "top_p": 0.8, + "top_k": 32, + } + assert provider.supports_sampling_option("temperature") is True + assert provider.supports_sampling_option("top_p") is True + assert provider.supports_sampling_option("top_k") is True + + +def test_anthropic_default_max_tokens_scales_with_model_family(): + provider = object.__new__(AnthropicProvider) + provider._options = {} + + provider._model_name = "claude-3-opus-20240229" + legacy = AnthropicProvider._build_message_params(provider, prompt="hi") + provider._model_name = "claude-sonnet-4-6" + modern = AnthropicProvider._build_message_params(provider, prompt="hi") + + assert legacy["max_tokens"] == 4096 + assert modern["max_tokens"] > legacy["max_tokens"] + + +@pytest.mark.parametrize( + ("model_name", "deprecated", "max_tokens"), + [ + ("claude-sonnet-4-20250514", False, 8192), + ("claude-sonnet-4-5-20250929", True, 16384), + ("claude-sonnet-5", True, 16384), + ("claude-3-7-sonnet-20250219", False, 8192), + ], +) +def test_anthropic_model_version_parser_ignores_dates_and_supports_major_only( + model_name, deprecated, max_tokens +): + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is deprecated + assert provider._default_max_tokens() == max_tokens + + +def test_anthropic_file_resources_prefer_stable_sdk_surface(): + provider = object.__new__(AnthropicProvider) + calls = [] + + class Files: + def upload(self, **kwargs): + calls.append(("stable", kwargs)) + return "stable" + + class BetaFiles: + def upload(self, **_kwargs): + raise AssertionError("beta files should not be used for stable file operations") + + provider._get_client = lambda: SimpleNamespace( + files=Files(), beta=SimpleNamespace(files=BetaFiles()) + ) + + assert provider.upload_file(b"data") == "stable" + assert calls == [("stable", {"file": b"data"})] + + +def test_google_cache_and_batch_resources_supply_default_model(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Caches: + def create(self, **kwargs): + calls.append(("cache", kwargs)) + return "cache" + + class Batches: + def create(self, **kwargs): + calls.append(("batch", kwargs)) + return "batch" + + provider._get_client = lambda: SimpleNamespace(caches=Caches(), batches=Batches()) + assert provider.resources.create_cache(config={}) == "cache" + assert provider.resources.create_batch([]) == "batch" + assert calls == [ + ("cache", {"model": "gemini-default", "config": {}}), + ("batch", {"model": "gemini-default", "src": []}), + ] + + +def test_google_async_stream_and_chat_contracts_do_not_double_await(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + class AsyncModels: + async def generate_content_stream(self, **_kwargs): + async def chunks(): + yield SimpleNamespace( + candidates=[SimpleNamespace(finish_reason="STOP")] + ) + + return chunks() + + class AsyncChats: + def create(self, **kwargs): + return ("chat", kwargs) + + client = SimpleNamespace( + aio=SimpleNamespace(models=AsyncModels(), chats=AsyncChats()) + ) + provider._get_client = lambda: client + events = asyncio.run(_collect_google_async_events(provider)) + assert [event.type for event in events] == ["finish"] + assert asyncio.run(provider.async_create_chat(history=[])) == ( + "chat", {"model": "gemini", "history": []} + ) + + +def test_google_async_cache_and_batch_resources_supply_default_model(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class AsyncCaches: + async def create(self, **kwargs): + calls.append(("cache", kwargs)) + return "cache" + + class AsyncBatches: + async def create(self, **kwargs): + calls.append(("batch", kwargs)) + return "batch" + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(caches=AsyncCaches(), batches=AsyncBatches()) + ) + assert asyncio.run(provider.async_resources.create_cache(config={})) == "cache" + assert asyncio.run(provider.async_resources.create_batch([])) == "batch" + assert calls == [ + ("cache", {"model": "gemini-default", "config": {}}), + ("batch", {"model": "gemini-default", "src": []}), + ] + + +def test_google_tuning_and_file_search_resources_forward_current_sdk_methods(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Tunings: + def tune(self, **kwargs): + calls.append(("tune", kwargs)) + return "tuning" + + def validate_reward(self, **kwargs): + calls.append(("reward", kwargs)) + return "reward" + + class FileSearchStores: + def import_file(self, **kwargs): + calls.append(("import", kwargs)) + return "imported" + + provider._get_client = lambda: SimpleNamespace(tunings=Tunings(), file_search_stores=FileSearchStores()) + assert provider.resources.tune("gemini-base", {"examples": []}, config={}) == "tuning" + assert provider.resources.validate_tuning_reward("jobs/1", {"text": "ok"}, {"input": "x"}) == "reward" + assert provider.resources.import_file_to_file_search_store("stores/1", "files/1") == "imported" + assert calls == [ + ( + "tune", + { + "base_model": "gemini-base", + "training_dataset": {"examples": []}, + "config": {}, + }, + ), + ("reward", {"parent": "jobs/1", "sample_response": {"text": "ok"}, "example": {"input": "x"}}), + ("import", {"file_search_store_name": "stores/1", "file_name": "files/1"}), + ] + + +def test_google_tuning_resource_missing_is_explicit(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="调优资源"): + provider.resources.list_tunings() + + +def test_resource_facade_rejects_sensitive_request_extensions_sync_and_async(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider.list_files = lambda **_: "should-not-run" + provider.async_list_files = lambda **_: "should-not-run" + + with pytest.raises(ValueError, match="凭证"): + provider.resources.list_files(extra_headers={"X-API-Key": "secret"}) + + with pytest.raises(ValueError, match="凭证"): + asyncio.run(provider.async_resources.list_files(extra_body={"token": "secret"})) + + +def test_direct_provider_resource_methods_reject_sensitive_extensions(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace( + files=SimpleNamespace(list=lambda **kwargs: kwargs) + ) + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_body={"nested": {"access_token": "secret"}}) + + +def test_dynamic_native_resource_proxy_validates_nested_calls_and_keeps_results(): + calls = [] + + class Files: + def list(self, **kwargs): + calls.append(kwargs) + return "page" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + native = SimpleNamespace(experimental=SimpleNamespace(files=Files())) + provider._get_client = lambda: native + + assert provider.resources.experimental.files.list(limit=2) == "page" + assert calls == [{"limit": 2}] + with pytest.raises(ValueError, match="凭证"): + provider.resources.experimental.files.list( + extra_query={"access_token": "secret"} + ) + + +def test_dynamic_native_resource_proxy_rejects_sensitive_positional_arguments(): + calls = [] + + class Responses: + def connect(self, *args, **kwargs): + calls.append((args, kwargs)) + return "connection" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.resources.responses.connect({"tenant": "demo"}, {"X-Trace": "1"}) == "connection" + assert calls == [ + (({"tenant": "demo"}, {"X-Trace": "1"}), {}), + ] + for args in ( + ({"Authorization": "Bearer secret"},), + ({"nested": {"access_token": "secret"}},), + ("https://api.example.test/v1?api_key=secret",), + ): + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider.resources.responses.connect(*args) + + +def test_direct_openai_resource_call_rejects_sensitive_positional_arguments(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + + def connect(*args, **kwargs): + return args, kwargs + + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider._call_sdk_resource( + connect, + args=({"extra_headers": {"Authorization": "Bearer secret"}},), + operation="OpenAI Responses 连接", + ) + assert provider._call_sdk_resource( + connect, + args=("response-id",), + operation="OpenAI Responses 连接", + ) == (("response-id",), {}) + + +def test_provider_resource_guard_rejects_sensitive_positional_arguments(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + + class Models: + def update(self, **_kwargs): + raise AssertionError("SDK should not be called") + + provider._get_client = lambda: SimpleNamespace(models=Models()) + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider.update_model("models/demo", {"Authorization": "Bearer secret"}) + + +def test_provider_resource_guard_normalizes_duplicate_sdk_arguments(): + provider = object.__new__(GoogleProvider) + + class Interactions: + def get(self, *, id): + return id + + provider._get_client = lambda: SimpleNamespace(interactions=Interactions()) + with pytest.raises(ValueError, match="资源 SDK 参数重复"): + provider.get_interaction("interaction-1", id="interaction-2") + + +def test_resource_argument_validator_keeps_nested_business_values_isolated(): + original = {"query": {"text": "hello"}} + validated = validate_secret_free_resource_args((original,), "Provider") + assert validated == (original,) + assert validated[0] is not original + original["query"]["text"] = "changed" + assert validated[0]["query"]["text"] == "hello" + + +def test_resource_kwargs_reject_duplicate_extension_spellings(): + with pytest.raises(ValueError, match="重复的扩展字段"): + validate_secret_free_resource_kwargs( + {"extra_headers": {"X-Trace": "1"}, "EXTRA_HEADERS": {"X-Trace": "2"}}, + "Provider", + ) + + +def test_dynamic_native_resource_proxy_preserves_native_client_identity(): + provider = object.__new__(OpenAIProvider) + native = SimpleNamespace(experimental=SimpleNamespace(value="ok")) + provider._get_client = lambda: native + + assert provider.resources.native is native + assert provider.resources.experimental.value == "ok" + + +def test_dynamic_native_vector_store_search_allows_business_query_but_scans_nested_credentials(): + calls = [] + + class VectorStores: + def search(self, **kwargs): + calls.append(kwargs) + return "page" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace(vector_stores=VectorStores()) + + assert provider.resources.vector_stores.search( + vector_store_id="vs_1", query="refund" + ) == "page" + assert calls == [{"vector_store_id": "vs_1", "query": "refund"}] + + with pytest.raises(ValueError, match="凭证"): + provider.resources.vector_stores.search( + vector_store_id="vs_1", query={"Authorization": "Bearer secret"} + ) + + +def test_dynamic_native_resource_proxy_wraps_with_options_results_and_guards_credentials(): + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(("create", kwargs)) + return "response" + + class Client: + def __init__(self): + self.responses = Responses() + + def with_options(self, **kwargs): + calls.append(("with_options", kwargs)) + return Client() + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = Client + + configured = provider.resources.with_options(timeout=2) + assert configured.responses.create(model="demo") == "response" + assert calls == [ + ("with_options", {"timeout": 2}), + ("create", {"model": "demo"}), + ] + + for key in ("api_key", "admin_api_key", "webhook_secret", "default_headers", "base_url"): + with pytest.raises(ValueError, match="凭证或请求头/query"): + provider.resources.with_options(**{key: "secret"}) + + with pytest.raises(ValueError, match="凭证"): + configured.responses.create(extra_headers={"Authorization": "Bearer secret"}) + + +def test_dynamic_async_native_resource_proxy_guards_nested_raw_and_streaming_resources(): + calls = [] + + class RawResponses: + async def create(self, **kwargs): + calls.append(("raw", kwargs)) + return "raw-response" + + class StreamingResponses: + async def create(self, **kwargs): + calls.append(("streaming", kwargs)) + return "streaming-response" + + class Responses: + def __init__(self): + self._raw = RawResponses() + self._streaming = StreamingResponses() + + @property + def with_raw_response(self): + return self._raw + + @property + def with_streaming_response(self): + return self._streaming + + class Client: + def __init__(self): + self.responses = Responses() + + def with_options(self, **kwargs): + calls.append(("with_options", kwargs)) + return self + + def copy(self, **kwargs): + calls.append(("copy", kwargs)) + return self + + client = Client() + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_aclient = lambda: client + + configured = provider.async_resources.with_options(timeout=2) + copied = configured.copy(max_retries=1) + assert asyncio.run(copied.responses.with_raw_response.create(model="demo")) == "raw-response" + assert asyncio.run( + copied.responses.with_streaming_response.create(model="demo") + ) == "streaming-response" + assert calls[:2] == [("with_options", {"timeout": 2}), ("copy", {"max_retries": 1})] + + with pytest.raises(ValueError, match="凭证"): + copied.responses.with_raw_response.create( + extra_headers={"Authorization": "Bearer secret"} + ) + with pytest.raises(ValueError, match="凭证"): + copied.responses.with_streaming_response.create( + extra_body={"nested": {"access_token": "secret"}} + ) + + for helper in ("with_options", "copy"): + with pytest.raises(ValueError, match="凭证或请求头/query"): + getattr(provider.async_resources, helper)(default_query={"token": "secret"}) + + +def test_google_tuning_facades_forward_additional_sdk_kwargs(): + calls = [] + + class Provider: + def tune(self, base_model, training_dataset, config=None, **kwargs): + calls.append(("sync", base_model, training_dataset, config, kwargs)) + return "tuning" + + async def async_tune(self, base_model, training_dataset, config=None, **kwargs): + calls.append(("async", base_model, training_dataset, config, kwargs)) + return "async-tuning" + + provider = Provider() + assert GoogleResources(provider).tune( + "gemini-base", {"examples": []}, labels={"suite": "test"} + ) == "tuning" + assert asyncio.run( + AsyncGoogleResources(provider).tune( + "gemini-base", {"examples": []}, labels={"suite": "test"} + ) + ) == "async-tuning" + assert calls == [ + ("sync", "gemini-base", {"examples": []}, None, {"labels": {"suite": "test"}}), + ("async", "gemini-base", {"examples": []}, None, {"labels": {"suite": "test"}}), + ] + + +def test_google_next_generation_resources_forward_sync_and_async_calls(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Resource: + def __getattr__(self, name): + def method(*args, **kwargs): + calls.append((name, args, kwargs)) + return f"{name}-result" + + return method + + provider._get_client = lambda: SimpleNamespace( + interactions=Resource(), + agents=Resource(), + webhooks=Resource(), + environments=Resource(), + triggers=Resource(), + aio=SimpleNamespace( + interactions=Resource(), + agents=Resource(), + webhooks=Resource(), + environments=Resource(), + triggers=Resource(), + live=SimpleNamespace(connect=lambda **kwargs: ("live", kwargs)), + ), + ) + + assert provider.resources.create_interaction(input="hello") == "create-result" + assert provider.resources.get_webhook("wh-1") == "get-result" + assert provider.resources.run_trigger("tr-1", payload={"x": 1}) == "run-result" + assert provider.resources.connect_live(config={}) == ("live", {"model": "gemini-default", "config": {}}) + + async def collect(): + return ( + await provider.async_resources.create_interaction(input="hello"), + await provider.async_resources.get_agent("agent-1"), + await provider.async_resources.delete_environment("env-1"), + await provider.async_resources.list_trigger_executions("tr-1"), + ) + + assert asyncio.run(collect()) == ( + "create-result", + "get-result", + "delete_environment-result", + "list_executions-result", + ) + assert ("create", (), {"input": "hello"}) in calls + assert ("get", ("wh-1",), {}) in calls + + +def test_provider_resource_facades_expose_beta_trees_without_copying_sdk_methods(): + anthropic_provider = object.__new__(AnthropicProvider) + anthropic_client = SimpleNamespace(beta=SimpleNamespace(messages="beta-messages")) + anthropic_provider._get_client = lambda: anthropic_client + anthropic_provider._get_aclient = lambda: SimpleNamespace(beta="async-beta") + assert anthropic_provider.resources.beta.messages == "beta-messages" + assert anthropic_provider.async_resources.async_beta == "async-beta" + + compatible = object.__new__(OpenAICompatibleProvider) + compatible._get_client = lambda: SimpleNamespace(beta="should-not-be-visible") + compatible._get_aclient = lambda: SimpleNamespace(beta="should-not-be-visible") + assert not hasattr(compatible.resources, "beta") + assert not hasattr(compatible.resources, "async_beta") + + ark_provider = object.__new__(VolcengineProvider) + ark_calls = [] + + class Completions: + def parse(self, **kwargs): + ark_calls.append(("parse", kwargs)) + return "parsed" + + def stream(self, **kwargs): + ark_calls.append(("stream", kwargs)) + return "stream" + + ark_provider._model_name = "ark-model" + ark_provider._get_client = lambda: SimpleNamespace( + beta=SimpleNamespace(chat=SimpleNamespace(completions=Completions())), + bot_chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **kwargs: ("bot", kwargs))), + ) + assert ark_provider.resources.beta_chat_parse(messages=[], response_format={}) == "parsed" + assert ark_provider.resources.beta_chat_stream(messages=[]) == "stream" + assert ark_provider.resources.bot_chat(messages=[]) == ("bot", {"messages": [], "model": "ark-model"}) + assert ark_calls == [ + ("parse", {"messages": [], "response_format": {}, "model": "ark-model"}), + ("stream", {"messages": [], "model": "ark-model"}), + ] + + +@pytest.mark.parametrize( + "method, args, kwargs", + [ + ("upload_file", (b"data", "user_data"), {"extra_headers": {"X-API-Key": "secret"}}), + ("list_files", (), {"extra_query": {"access_token": "secret"}}), + ("retrieve_response", ("resp-1",), {"extra_body": {"token": "secret"}}), + ("create_content_generation_task", (), {"content": [], "extra_body": {"password": "secret"}}), + ("beta_chat_parse", (), {"messages": [], "extra_query": {"api_key": "secret"}}), + ("bot_chat", (), {"messages": [], "extra_headers": {"Authorization": "Bearer secret"}}), + ("classify", ("query", ["label"]), {"extra_headers": {"X-Api-Key": "secret"}}), + ], +) +def test_ark_resource_methods_reject_sensitive_extensions(method, args, kwargs): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._model_name = "ark-model" + + class Resource: + def __getattr__(self, _name): + return lambda **_kwargs: "should-not-run" + + provider._get_client = lambda: SimpleNamespace( + files=Resource(), + responses=Resource(), + content_generation=SimpleNamespace(tasks=Resource()), + beta=SimpleNamespace(chat=SimpleNamespace(completions=Resource())), + bot_chat=SimpleNamespace(completions=Resource()), + classification=Resource(), + ) + + with pytest.raises(ValueError, match="凭证"): + getattr(provider, method)(*args, **kwargs) + + +def test_ark_async_beta_chat_stream_returns_async_context_manager_directly(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class StreamManager: + def __init__(self): + self.entered = False + self.exited = False + + async def __aenter__(self): + self.entered = True + return "stream" + + async def __aexit__(self, exc_type, exc_value, traceback): + self.exited = True + + stream_manager = StreamManager() + + class Completions: + def stream(self, **kwargs): + calls.append(kwargs) + return stream_manager + + provider._get_aclient = lambda: SimpleNamespace( + beta=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + + result = provider.async_resources.beta_chat_stream(messages=[]) + + assert result is stream_manager + assert not inspect.isawaitable(result) + assert calls == [{"messages": [], "model": "ark-model"}] + + async def consume_stream(): + async with result as stream: + return stream + + assert asyncio.run(consume_stream()) == "stream" + assert stream_manager.entered is True + assert stream_manager.exited is True + + +async def _collect_google_async_events(provider): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hi"))] + + +def test_ark_async_input_items_use_top_level_resource(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = {} + + class InputItems: + def list(self, response_id, **kwargs): + calls["args"] = (response_id, kwargs) + return "items" + + provider._get_aclient = lambda: SimpleNamespace(input_items=InputItems()) + assert asyncio.run(provider.async_list_response_input_items("resp-1", limit=2)) == "items" + assert calls["args"] == ("resp-1", {"limit": 2}) + + +@pytest.mark.parametrize( + "method_name, args, kwargs", + [ + ("retrieve_response", ("resp-1",), {"include": ["output"]}), + ("delete_response", ("resp-1",), {"include": ["output"]}), + ("list_response_input_items", ("resp-1",), {"bogus": True}), + ("list_input_items", ("resp-1",), {"bogus": True}), + ], +) +def test_ark_response_resources_reject_unknown_kwargs_before_sdk_call( + method_name, args, kwargs +): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_client = lambda: pytest.fail("不支持的 Ark Responses 参数不应调用 SDK") + + with pytest.raises(ValueError, match="不支持请求参数"): + getattr(provider, method_name)(*args, **kwargs) + + +@pytest.mark.parametrize( + "method_name, args, kwargs", + [ + ("async_retrieve_response", ("resp-1",), {"include": ["output"]}), + ("async_delete_response", ("resp-1",), {"include": ["output"]}), + ("async_list_response_input_items", ("resp-1",), {"bogus": True}), + ("async_list_input_items", ("resp-1",), {"bogus": True}), + ], +) +def test_ark_async_response_resources_reject_unknown_kwargs_before_sdk_call( + method_name, args, kwargs +): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_aclient = lambda: pytest.fail("不支持的 Ark Responses 参数不应调用 SDK") + + async def invoke(): + await getattr(provider, method_name)(*args, **kwargs) + + with pytest.raises(ValueError, match="不支持请求参数"): + asyncio.run(invoke()) + + +def test_ark_async_chat_stream_rejects_incomplete_tool_call(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", + index=0, + type="function", + function=SimpleNamespace( + name="lookup", arguments="" + ), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=None, tool_calls=None), + finish_reason="tool_calls", + ) + ] + ), + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hi"))] + + with pytest.raises(RuntimeError, match="完整 arguments"): + asyncio.run(collect()) + + +def test_ark_responses_request_and_stream(monkeypatch): + settings = SimpleNamespace( + volc_access_key=None, + volc_secret_key=None, + ark_api_key="ark-key", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + provider = VolcengineProvider( + "doubao", + protocol="responses", + options={"server_verified_protocols": ["responses"]}, + ) + assert provider._build_responses_request(CompletionRequest(prompt="hi", stream=True))["input"] == "hi" + + class Responses: + def create(self, **_kwargs): + return iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="ok"), + SimpleNamespace(type="response.completed", response=SimpleNamespace(status="completed")), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert list(provider.invoke("hi")) == ["ok"] + + +def test_ark_responses_direct_invocation_requires_server_verification(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {} + provider._get_client = lambda: pytest.fail("未验证的 Ark Responses 不应初始化客户端") + + with pytest.raises(ValueError, match="server_verified_protocols"): + list(provider.invoke("hello", stream=False)) + + +def test_factory_ark_responses_requires_explicit_server_verification(monkeypatch): + settings = SimpleNamespace( + volc_access_key=None, + volc_secret_key=None, + ark_api_key="ark-key", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + monkeypatch.setattr( + ModelProviderFactory, + "_get_provider_class", + staticmethod(lambda _provider_name: VolcengineProvider), + ) + + with pytest.raises(ValueError, match="server_verified_protocols"): + ModelProviderFactory._create_provider( + "volcengine", + "doubao-model", + "llm", + LargeLanguageModel, + protocol="responses", + options={}, + ) + + provider = ModelProviderFactory._create_provider( + "volcengine", + "doubao-model", + "llm", + LargeLanguageModel, + protocol="responses", + options={"server_verified_protocols": ["responses"]}, + ) + assert isinstance(provider, VolcengineProvider) + + +def test_ark_ainvoke_preserves_explicit_none_system_prompt(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def consume(): + return [item async for item in provider.ainvoke( + "hello", system_prompt=None, stream=False + )] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["messages"] == [{"role": "user", "content": "hello"}] + + +def test_ark_responses_normalizes_model_options_to_native_shape(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "max_tokens": 1024, + "response_format": {"type": "json_object"}, + "reasoning_effort": "high", + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False) + ) + + assert request["max_output_tokens"] == 1024 + assert request["text"] == {"format": {"type": "json_object"}} + assert request["reasoning"] == {"effort": "high"} + assert "max_tokens" not in request + assert "response_format" not in request + assert "reasoning_effort" not in request + + +def test_ark_responses_converts_configured_tool_choice_to_native_shape(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "tool_choice": {"type": "function", "function": {"name": "lookup"}}, + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False) + ) + + assert request["tool_choice"] == {"type": "function", "name": "lookup"} + + +def test_ark_chat_model_options_move_sampling_extensions_into_extra_body(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"top_k": 20, "seed": 7} + + request = provider._build_chat_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"top_k": 20, "seed": 7} + assert "top_k" not in request + assert "seed" not in request + + +@pytest.mark.parametrize( + "tool_choice", + [ + {"type": "function", "function": "lookup"}, + {"type": "function", "function": 1}, + ], +) +def test_ark_responses_tool_choice_rejects_non_object_function(tool_choice): + with pytest.raises(ValueError, match="function tool_choice"): + VolcengineProvider._convert_responses_tool_choice(tool_choice) + + +def test_ark_response_resource_create_uses_unified_request_and_native_defaults(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return "response" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert provider.resources.create_response(CompletionRequest(prompt="hi", stream=False)) == "response" + assert calls == [{ + "model": "ark-model", + "input": "hi", + "stream": False, + "instructions": "You are a helpful assistant.", + }] + with pytest.raises(ValueError, match="model"): + provider.resources.create_response(input="raw") + assert provider.resources.create_response(input="raw", model="caller-model") == "response" + assert calls[-1] == {"input": "raw", "model": "caller-model"} + + +def test_ark_async_native_response_resource_requires_explicit_model_and_preserves_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "configured-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + async def create(self, **kwargs): + calls.append(kwargs) + return "async-response" + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def invoke(): + with pytest.raises(ValueError, match="model"): + await provider.async_resources.create_response(input="raw") + result = await provider.async_resources.create_response( + input="raw", model="caller-model", stream=False + ) + return result + + assert asyncio.run(invoke()) == "async-response" + assert calls == [{"input": "raw", "model": "caller-model", "stream": False}] + + +def test_google_environment_resource_names_and_async_live_contracts(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Files: + def list(self, environment, path, **kwargs): + calls.append(("files", (environment, path), kwargs)) + return "files" + + class AsyncFiles: + async def list(self, environment, path, **kwargs): + calls.append(("async-files", (environment, path), kwargs)) + return "async-files" + + class Environments: + def create_environment(self, **kwargs): + calls.append(("create", (), kwargs)) + return "created" + + def get_environment(self, **kwargs): + calls.append(("get", (), kwargs)) + return "environment" + + def list_environments(self, **kwargs): + calls.append(("list", (), kwargs)) + return "environments" + + def delete_environment(self, **kwargs): + calls.append(("delete", (), kwargs)) + return "deleted" + + files = Files() + + class AsyncEnvironments(Environments): + files = AsyncFiles() + + class Live: + def connect(self, **kwargs): + calls.append(("live", (), kwargs)) + return "live-manager" + + provider._get_client = lambda: SimpleNamespace( + environments=Environments(), + aio=SimpleNamespace( + environments=AsyncEnvironments(), + live=Live(), + ), + ) + assert provider.resources.create_environment(network={}) == "created" + assert provider.resources.get_environment("env-1") == "environment" + assert provider.resources.list_environments(page_size=10) == "environments" + assert provider.resources.delete_environment("env-1") == "deleted" + assert provider.resources.get_environment_files("env-1", "src", recursive=True) == "files" + assert provider.async_resources.connect_live(config={}) == "live-manager" + assert asyncio.run( + provider.async_resources.get_environment_files("env-1", "src", recursive=True) + ) == "async-files" + assert calls == [ + ("create", (), {"network": {}}), + ("get", (), {"id": "env-1"}), + ("list", (), {"page_size": 10}), + ("delete", (), {"id": "env-1"}), + ("files", ("env-1", "src"), {"recursive": True}), + ("live", (), {"model": "gemini-default", "config": {}}), + ("async-files", ("env-1", "src"), {"recursive": True}), + ] + + +@pytest.mark.parametrize( + "method_name", + [ + "create_auth_token", + "create_interaction", + "create_agent", + "create_webhook", + "create_environment", + "create_trigger", + ], +) +def test_google_experimental_resources_fail_with_capability_error_when_sdk_missing(method_name): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="google-genai SDK"): + getattr(provider, method_name)() + + +@pytest.mark.parametrize( + "method_name", + [ + "async_create_auth_token", + "async_create_interaction", + "async_create_agent", + "async_create_webhook", + "async_create_environment", + "async_create_trigger", + ], +) +def test_google_async_experimental_resources_fail_with_capability_error_when_sdk_missing(method_name): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace()) + + async def invoke(): + with pytest.raises(NotImplementedError, match="google-genai SDK"): + await getattr(provider, method_name)() + + asyncio.run(invoke()) + + +def test_google_live_connection_rejects_credentials_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace( + live=SimpleNamespace(connect=lambda **_: "should-not-run"), + ) + ) + config = {"http_options": {"headers": {"Authorization": "Bearer secret"}}} + with pytest.raises(ValueError, match="凭证"): + provider.connect_live(config=config) + with pytest.raises(ValueError, match="凭证"): + provider.async_connect_live(config=config) + + +def test_google_live_connection_reports_missing_sdk_resource(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace()) + with pytest.raises(NotImplementedError, match="Live"): + provider.connect_live() + + +def test_ark_content_generation_delete_uses_keyword_task_id_for_sync_and_async(): + provider = object.__new__(VolcengineProvider) + calls = [] + + class Tasks: + def delete(self, *, task_id, **kwargs): + calls.append(("sync", task_id, kwargs)) + return "deleted" + + class AsyncTasks: + async def delete(self, *, task_id, **kwargs): + calls.append(("async", task_id, kwargs)) + return "async-deleted" + + provider._get_client = lambda: SimpleNamespace(content_generation=SimpleNamespace(tasks=Tasks())) + provider._get_aclient = lambda: SimpleNamespace(content_generation=SimpleNamespace(tasks=AsyncTasks())) + + assert provider.resources.delete_content_generation_task("task-1", timeout=4) == "deleted" + assert asyncio.run(provider.async_resources.delete_content_generation_task("task-2", timeout=5)) == "async-deleted" + assert calls == [ + ("sync", "task-1", {"timeout": 4}), + ("async", "task-2", {"timeout": 5}), + ] + + +def test_ark_batch_chat_uses_current_nested_batch_resource(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = {} + + class Completions: + def create(self, **kwargs): + calls.update(kwargs) + return "batch" + + provider._get_client = lambda: SimpleNamespace( + batch=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + assert provider.resources.create_batch_chat(messages=[]) == "batch" + assert calls == {"messages": [], "model": "ark-model"} + + +def test_ark_async_batch_chat_forwards_sdk_supported_user_argument(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return "async-batch" + + provider._get_aclient = lambda: SimpleNamespace( + batch=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + + assert asyncio.run( + provider.async_create_batch_chat(messages=[], user="user-1") + ) == "async-batch" + assert calls == [{"messages": [], "model": "ark-model", "user": "user-1"}] + + +def test_ark_context_resources_validate_sdk_parameter_names_and_defaults(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class Context: + def create(self, **kwargs): + calls.append(("create", kwargs)) + return "context" + + class Completions: + def create(self, **kwargs): + calls.append(("complete", kwargs)) + return "completion" + + completions = Completions() + + provider._get_client = lambda: SimpleNamespace(context=Context()) + + assert provider.create_context(messages=[], mode="session") == "context" + assert provider.context_complete(context_id="ctx-1", messages=[], stream=False) == "completion" + assert calls == [ + ("create", {"messages": [], "mode": "session", "model": "ark-model"}), + ("complete", {"context_id": "ctx-1", "messages": [], "stream": False, "model": "ark-model"}), + ] + + with pytest.raises(ValueError, match="Context.*temperature"): + provider.create_context(messages=[], temperature=0.2) + with pytest.raises(ValueError, match="Context Completion.*unknown"): + provider.context_complete(context_id="ctx-1", messages=[], unknown=None) + + +def test_ark_classification_validates_query_labels_model_and_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._classification_resource = SimpleNamespace(create=lambda **kwargs: kwargs) + provider._get_client = lambda: SimpleNamespace() + + result = provider.classify(" refund ", [" billing ", "support"]) + assert result == { + "query": "refund", + "model": "ark-model", + "labels": ["billing", "support"], + } + with pytest.raises(ValueError, match="query"): + provider.classify(" ", ["support"]) + with pytest.raises(ValueError, match="labels"): + provider.classify("refund", []) + with pytest.raises(ValueError, match="Classification.*unknown"): + provider.classify("refund", ["support"], unknown=None) + + +def test_ark_batch_multimodal_embedding_rejects_sparse_embedding_before_sdk_call(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + + class FailingResource: + def create(self, **_kwargs): + raise AssertionError("不应调用不支持 sparse_embedding 的 SDK 方法") + + provider._get_client = lambda: SimpleNamespace( + batch=SimpleNamespace(multimodal_embeddings=FailingResource()) + ) + provider._get_aclient = lambda: SimpleNamespace( + batch=SimpleNamespace(multimodal_embeddings=FailingResource()) + ) + + with pytest.raises(ValueError, match="Ark Batch Multimodal Embedding 不支持请求参数: sparse_embedding"): + provider.create_batch_multimodal_embedding(input=[], sparse_embedding={"enabled": True}) + + with pytest.raises(ValueError, match="Ark Batch Multimodal Embedding 不支持请求参数: sparse_embedding"): + asyncio.run( + provider.async_create_batch_multimodal_embedding( + input=[], sparse_embedding={"enabled": True} + ) + ) + + +def test_ark_async_generate_batch_chat_uses_matching_provider_method(): + provider = object.__new__(VolcengineProvider) + calls = [] + + async def create_batch_chat(**kwargs): + calls.append(("create", kwargs)) + return "created" + + async def generate_batch_chat(**kwargs): + calls.append(("generate", kwargs)) + return "generated" + + provider.async_create_batch_chat = create_batch_chat + provider.async_generate_batch_chat = generate_batch_chat + + result = asyncio.run(provider.async_resources.generate_batch_chat(messages=[])) + + assert result == "generated" + assert calls == [("generate", {"messages": []})] + + +def test_ark_tokenization_and_multimodal_embedding_reject_unknown_sdk_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {} + provider._get_client = lambda: SimpleNamespace() + with pytest.raises(ValueError, match="Tokenization 不支持请求参数: temperature"): + provider.count_tokens("hello", temperature=0.2) + with pytest.raises(ValueError, match="Multimodal Embedding 不支持请求参数: temperature"): + provider.multimodal_embed([], temperature=0.2) + + +def test_openai_responses_stream_helper_matches_installed_sdk_contract_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + calls = [] + + class Responses: + def stream(self, **kwargs): + calls.append(("sync", kwargs)) + return "sync-stream" + + class AsyncResponses: + def stream(self, **kwargs): + calls.append(("async", kwargs)) + return "async-stream" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + assert provider.stream_responses(CompletionRequest(prompt="hi", stream=True)) == "sync-stream" + assert provider.async_stream_responses(CompletionRequest(prompt="hi", stream=True)) == "async-stream" + assert calls == [ + ("sync", { + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + }), + ("async", { + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + }), + ] + + assert provider.stream_responses( + input="hi", safety_identifier="user-1" + ) == "sync-stream" + assert calls[-1] == ( + "sync", + {"input": "hi", "model": "demo", "safety_identifier": "user-1"}, + ) + assert provider.stream_responses(input="hi", verbosity="low") == "sync-stream" + assert calls[-1] == ( + "sync", + {"input": "hi", "model": "demo", "text": {"verbosity": "low"}}, + ) + + +def test_openai_responses_stream_continuation_omits_model_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + calls = [] + + class Responses: + def stream(self, **kwargs): + if "response_id" in kwargs or "starting_after" in kwargs: + assert "model" not in kwargs + calls.append(("sync", kwargs)) + return "sync-stream" + + class AsyncResponses: + def stream(self, **kwargs): + if "response_id" in kwargs or "starting_after" in kwargs: + assert "model" not in kwargs + calls.append(("async", kwargs)) + return "async-stream" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + assert provider.stream_responses(response_id="resp-1") == "sync-stream" + assert calls[-1] == ("sync", {"response_id": "resp-1"}) + assert ( + provider.stream_responses(response_id="resp-1", starting_after=2) + == "sync-stream" + ) + assert calls[-1] == ( + "sync", + {"response_id": "resp-1", "starting_after": 2}, + ) + + assert ( + provider.async_stream_responses(response_id="resp-1", starting_after=2) + == "async-stream" + ) + assert calls[-1] == ( + "async", + {"response_id": "resp-1", "starting_after": 2}, + ) + + + +def test_openai_polling_helpers_match_installed_sdk_parameters_sync(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + calls = [] + + class Files: + def wait_for_processing(self, **kwargs): + calls.append(("wait", kwargs)) + return "file" + + class VectorFiles: + def poll(self, *args, **kwargs): + calls.append(("vector_file", args, kwargs)) + return "vector-file" + + class VectorBatches: + def poll(self, *args, **kwargs): + calls.append(("vector_batch", args, kwargs)) + return "vector-batch" + + def upload_and_poll(self, **kwargs): + calls.append(("upload_batch", kwargs)) + return "uploaded-batch" + + class Videos: + def poll(self, *args, **kwargs): + calls.append(("video", args, kwargs)) + return "video" + + provider._get_client = lambda: SimpleNamespace( + files=Files(), + vector_stores=SimpleNamespace(files=VectorFiles(), file_batches=VectorBatches()), + videos=Videos(), + ) + + assert provider.wait_for_file("file-1", poll_interval=1.5, max_wait_seconds=20) == "file" + assert provider.poll_vector_store_file("store-1", "file-1", poll_interval_ms=250) == "vector-file" + assert provider.poll_vector_store_file_batch("store-1", "batch-1", poll_interval_ms=300) == "vector-batch" + assert provider.upload_vector_store_file_batch_and_poll( + "store-1", ["a"], max_concurrency=2, file_ids=["f-1"], + poll_interval_ms=400, chunking_strategy={"type": "auto"}, + ) == "uploaded-batch" + assert provider.poll_video("video-1", poll_interval_ms=500) == "video" + + assert calls == [ + ("wait", {"id": "file-1", "poll_interval": 1.5, "max_wait_seconds": 20}), + ("vector_file", ("file-1",), {"vector_store_id": "store-1", "poll_interval_ms": 250}), + ("vector_batch", ("batch-1",), {"vector_store_id": "store-1", "poll_interval_ms": 300}), + ("upload_batch", { + "vector_store_id": "store-1", "files": ["a"], "max_concurrency": 2, + "file_ids": ["f-1"], "poll_interval_ms": 400, + "chunking_strategy": {"type": "auto"}, + }), + ("video", ("video-1",), {"poll_interval_ms": 500}), + ] + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="向量库文件轮询.*extra_headers"): + provider.poll_vector_store_file("store-1", "file-1", extra_headers={"X-Test": "1"}) + with pytest.raises(ValueError, match="向量库文件批次上传轮询.*timeout"): + provider.upload_vector_store_file_batch_and_poll("store-1", [], timeout=1) + with pytest.raises(ValueError, match="视频轮询.*extra_query"): + provider.poll_video("video-1", extra_query={"trace": "1"}) + + +def test_openai_vector_store_upload_and_container_file_match_sdk_parameters(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def upload(self, **kwargs): + calls.append(("upload", kwargs)) + return "uploaded" + + class ContainerFiles: + def create(self, *args, **kwargs): + calls.append(("container", args, kwargs)) + return "container-file" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()), + containers=SimpleNamespace(files=ContainerFiles()), + ) + assert provider.upload_vector_store_file("store-1", "file", chunking_strategy={"type": "auto"}) == "uploaded" + assert provider.create_container_file("container-1", file_id="file-1") == "container-file" + assert calls == [ + ("upload", { + "vector_store_id": "store-1", "file": "file", + "chunking_strategy": {"type": "auto"}, + }), + ("container", ("container-1",), {"file_id": "file-1"}), + ] + with pytest.raises(ValueError, match="向量库文件上传.*timeout"): + provider.upload_vector_store_file("store-1", "file", timeout=1) + + +def test_openai_vector_store_upload_and_poll_matches_installed_sdk_parameters_sync(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def upload_and_poll(self, **kwargs): + calls.append(kwargs) + return "uploaded-and-polled" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + assert provider.upload_vector_store_file_and_poll( + "store-1", + "file", + attributes={"source": "docs", "rank": 1}, + poll_interval_ms=250, + chunking_strategy={"type": "auto"}, + ) == "uploaded-and-polled" + assert calls == [ + { + "vector_store_id": "store-1", + "file": "file", + "attributes": {"source": "docs", "rank": 1}, + "poll_interval_ms": 250, + "chunking_strategy": {"type": "auto"}, + } + ] + with pytest.raises(ValueError, match="向量库文件上传轮询.*timeout"): + provider.upload_vector_store_file_and_poll("store-1", "file", timeout=1) + + +def test_openai_vector_store_file_update_requires_sdk_attributes_parameter(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def update(self, *args, **kwargs): + calls.append((args, kwargs)) + return "updated" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + assert provider.update_vector_store_file( + "store-1", "file-1", attributes={"source": "docs", "rank": 1} + ) == "updated" + assert calls == [ + ( + ("file-1",), + { + "vector_store_id": "store-1", + "attributes": {"source": "docs", "rank": 1}, + }, + ) + ] + with pytest.raises(TypeError): + provider.update_vector_store_file("store-1", "file-1") + + +def test_openai_upload_file_chunked_matches_installed_sdk_contract_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class Uploads: + def upload_file_chunked(self, **kwargs): + calls.append(("sync", kwargs)) + return "uploaded" + + class AsyncUploads: + async def upload_file_chunked(self, **kwargs): + calls.append(("async", kwargs)) + return "async-uploaded" + + provider._get_client = lambda: SimpleNamespace(uploads=Uploads()) + provider._get_aclient = lambda: SimpleNamespace(uploads=AsyncUploads()) + + assert provider.upload_file_chunked( + file=b"payload", + mime_type="text/plain", + purpose="assistants", + filename="notes.txt", + bytes=7, + part_size=4, + md5="md5-value", + ) == "uploaded" + + async def run(): + return await provider.async_upload_file_chunked( + file=b"payload", + mime_type="text/plain", + purpose="assistants", + filename="notes.txt", + bytes=7, + part_size=4, + md5="md5-value", + ) + + assert asyncio.run(run()) == "async-uploaded" + assert calls == [ + ( + "sync", + { + "file": b"payload", + "mime_type": "text/plain", + "purpose": "assistants", + "filename": "notes.txt", + "bytes": 7, + "part_size": 4, + "md5": "md5-value", + }, + ), + ( + "async", + { + "file": b"payload", + "mime_type": "text/plain", + "purpose": "assistants", + "filename": "notes.txt", + "bytes": 7, + "part_size": 4, + "md5": "md5-value", + }, + ), + ] + + with pytest.raises(ValueError, match="分片文件上传.*timeout"): + provider.upload_file_chunked( + file=b"payload", mime_type="text/plain", purpose="assistants", timeout=1 + ) + + async def run_invalid(): + with pytest.raises(ValueError, match="分片文件上传.*timeout"): + await provider.async_upload_file_chunked( + file=b"payload", mime_type="text/plain", purpose="assistants", timeout=1 + ) + + asyncio.run(run_invalid()) + + +def test_openai_container_file_content_reports_missing_sdk_resource(): + provider = object.__new__(OpenAICompatibleProvider) + provider._get_client = lambda: SimpleNamespace( + containers=SimpleNamespace(files=SimpleNamespace()) + ) + + with pytest.raises(NotImplementedError, match="容器文件内容读取资源"): + provider.container_file_content("container-1", "file-1") + + +def test_openai_vector_store_upload_and_container_file_match_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + async def upload(self, **kwargs): + calls.append(("upload", kwargs)) + return "uploaded" + + class ContainerFiles: + async def create(self, *args, **kwargs): + calls.append(("container", args, kwargs)) + return "container-file" + + provider._get_aclient = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()), + containers=SimpleNamespace(files=ContainerFiles()), + ) + + async def run(): + assert await provider.async_upload_vector_store_file( + "store-1", "file", chunking_strategy={"type": "auto"} + ) == "uploaded" + assert await provider.async_create_container_file("container-1", file_id="file-1") == "container-file" + with pytest.raises(ValueError, match="向量库文件上传.*timeout"): + await provider.async_upload_vector_store_file("store-1", "file", timeout=1) + + asyncio.run(run()) + assert calls == [ + ("upload", { + "vector_store_id": "store-1", "file": "file", + "chunking_strategy": {"type": "auto"}, + }), + ("container", ("container-1",), {"file_id": "file-1"}), + ] + + +def test_openai_vector_store_upload_and_poll_matches_installed_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + async def upload_and_poll(self, **kwargs): + calls.append(kwargs) + return "uploaded-and-polled" + + provider._get_aclient = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + async def run(): + assert await provider.async_upload_vector_store_file_and_poll( + "store-1", + "file", + attributes={"source": "docs", "rank": 1}, + poll_interval_ms=250, + chunking_strategy={"type": "auto"}, + ) == "uploaded-and-polled" + with pytest.raises(ValueError, match="向量库文件上传轮询.*timeout"): + await provider.async_upload_vector_store_file_and_poll( + "store-1", "file", timeout=1 + ) + + asyncio.run(run()) + assert calls == [ + { + "vector_store_id": "store-1", + "file": "file", + "attributes": {"source": "docs", "rank": 1}, + "poll_interval_ms": 250, + "chunking_strategy": {"type": "auto"}, + } + ] + + +def test_openai_async_container_file_content_reports_missing_sdk_resource(): + provider = object.__new__(OpenAICompatibleProvider) + provider._get_aclient = lambda: SimpleNamespace( + containers=SimpleNamespace(files=SimpleNamespace()) + ) + + async def run(): + with pytest.raises(NotImplementedError, match="容器文件内容读取资源"): + await provider.async_container_file_content("container-1", "file-1") + + asyncio.run(run()) + + +def test_openai_polling_helpers_match_installed_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class Files: + async def wait_for_processing(self, **kwargs): + calls.append(("wait", kwargs)) + return "file" + + class VectorFiles: + async def poll(self, *args, **kwargs): + calls.append(("vector_file", args, kwargs)) + return "vector-file" + + class VectorBatches: + async def poll(self, *args, **kwargs): + calls.append(("vector_batch", args, kwargs)) + return "vector-batch" + + async def upload_and_poll(self, **kwargs): + calls.append(("upload_batch", kwargs)) + return "uploaded-batch" + + class Videos: + async def poll(self, *args, **kwargs): + calls.append(("video", args, kwargs)) + return "video" + + provider._get_aclient = lambda: SimpleNamespace( + files=Files(), + vector_stores=SimpleNamespace(files=VectorFiles(), file_batches=VectorBatches()), + videos=Videos(), + ) + + async def run(): + assert await provider.async_wait_for_file("file-1", poll_interval=1.5, max_wait_seconds=20) == "file" + assert await provider.async_poll_vector_store_file("store-1", "file-1", poll_interval_ms=250) == "vector-file" + assert await provider.async_poll_vector_store_file_batch("store-1", "batch-1", poll_interval_ms=300) == "vector-batch" + assert await provider.async_upload_vector_store_file_batch_and_poll( + "store-1", ["a"], max_concurrency=2, file_ids=["f-1"], + poll_interval_ms=400, chunking_strategy={"type": "auto"}, + ) == "uploaded-batch" + assert await provider.async_poll_video("video-1", poll_interval_ms=500) == "video" + with pytest.raises(ValueError, match="文件等待.*timeout"): + await provider.async_wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="向量库文件轮询.*extra_headers"): + await provider.async_poll_vector_store_file("store-1", "file-1", extra_headers={"X-Test": "1"}) + with pytest.raises(ValueError, match="向量库文件批次上传轮询.*timeout"): + await provider.async_upload_vector_store_file_batch_and_poll("store-1", [], timeout=1) + with pytest.raises(ValueError, match="视频轮询.*extra_query"): + await provider.async_poll_video("video-1", extra_query={"trace": "1"}) + + asyncio.run(run()) + assert calls == [ + ("wait", {"id": "file-1", "poll_interval": 1.5, "max_wait_seconds": 20}), + ("vector_file", ("file-1",), {"vector_store_id": "store-1", "poll_interval_ms": 250}), + ("vector_batch", ("batch-1",), {"vector_store_id": "store-1", "poll_interval_ms": 300}), + ("upload_batch", { + "vector_store_id": "store-1", "files": ["a"], "max_concurrency": 2, + "file_ids": ["f-1"], "poll_interval_ms": 400, + "chunking_strategy": {"type": "auto"}, + }), + ("video", ("video-1",), {"poll_interval_ms": 500}), + ] + + +def test_ark_file_wait_helpers_match_installed_sdk_parameters_sync_and_async(): + provider = object.__new__(VolcengineProvider) + calls = [] + + class Files: + def wait_for_processing(self, **kwargs): + calls.append(("sync", kwargs)) + return "file" + + class AsyncFiles: + async def wait_for_processing(self, **kwargs): + calls.append(("async", kwargs)) + return "async-file" + + provider._get_client = lambda: SimpleNamespace(files=Files()) + provider._get_aclient = lambda: SimpleNamespace(files=AsyncFiles()) + + assert provider.wait_for_file("file-1", poll_interval=2, max_wait_seconds=30) == "file" + assert asyncio.run( + provider.async_wait_for_file("file-2", poll_interval=1, max_wait_seconds=40) + ) == "async-file" + assert calls == [ + ("sync", {"id": "file-1", "poll_interval": 2, "max_wait_seconds": 30}), + ("async", {"id": "file-2", "poll_interval": 1, "max_wait_seconds": 40}), + ] + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="文件等待.*extra_headers"): + asyncio.run(provider.async_wait_for_file("file-2", extra_headers={"X-Test": "1"})) + + +@pytest.mark.parametrize( + ("file_id", "poll_interval", "max_wait_seconds", "match"), + [ + ("", 1, 10, "file_id"), + ("file-1", 0, 10, "poll_interval"), + ("file-1", float("inf"), 10, "poll_interval"), + ("file-1", 1, 0, "max_wait_seconds"), + ("file-1", 1, float("nan"), "max_wait_seconds"), + ], +) +def test_ark_file_wait_helpers_validate_identifiers_and_timeouts( + file_id, poll_interval, max_wait_seconds, match +): + provider = object.__new__(VolcengineProvider) + provider._get_client = lambda: pytest.fail("无效轮询参数不应调用 SDK") + provider._get_aclient = lambda: pytest.fail("无效轮询参数不应调用异步 SDK") + + with pytest.raises(ValueError, match=match): + provider.wait_for_file(file_id, poll_interval=poll_interval, max_wait_seconds=max_wait_seconds) + + with pytest.raises(ValueError, match=match): + asyncio.run( + provider.async_wait_for_file( + file_id, + poll_interval=poll_interval, + max_wait_seconds=max_wait_seconds, + ) + ) + + +def test_ark_file_wait_helpers_reject_unknown_none_arguments(): + provider = object.__new__(VolcengineProvider) + provider._get_client = lambda: pytest.fail("未知参数不应调用 SDK") + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=None) + + +def test_ark_classification_resource_forwards_sync_and_async_calls(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class Classification: + def create(self, **kwargs): + calls.append(("sync", kwargs)) + return "classified" + + class AsyncClassification: + async def create(self, **kwargs): + calls.append(("async", kwargs)) + return "async-classified" + + provider._get_client = lambda: SimpleNamespace() + provider._get_aclient = lambda: SimpleNamespace() + provider._classification_resource = Classification() + provider._async_classification_resource = AsyncClassification() + + assert provider.resources.classify("refund", ["billing", "support"]) == "classified" + assert asyncio.run( + provider.async_resources.classify("refund", ["billing", "support"]) + ) == "async-classified" + assert calls == [ + ("sync", {"query": "refund", "model": "ark-model", "labels": ["billing", "support"]}), + ("async", {"query": "refund", "model": "ark-model", "labels": ["billing", "support"]}), + ] + + +def test_ark_close_invalidates_cached_classification_resources(): + provider = object.__new__(VolcengineProvider) + provider._client = None + provider._aclient = None + provider._classification_resource = object() + provider._async_classification_resource = object() + + provider.close() + assert provider._classification_resource is None + assert provider._async_classification_resource is None + + provider._classification_resource = object() + provider._async_classification_resource = object() + asyncio.run(provider.aclose()) + assert provider._classification_resource is None + assert provider._async_classification_resource is None + + +def test_ark_classification_is_declared_as_provider_capability(): + assert "classification" in VolcengineProvider.capabilities + + +def test_ark_native_resource_wrappers_reject_credentials_before_sdk_dispatch(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + + def fail_if_called(): + raise AssertionError("SDK client must not be constructed for rejected credentials") + + provider._get_client = fail_if_called + provider._get_aclient = fail_if_called + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="凭证"): + provider.create_context(messages=[], extra_body={"token": "secret"}) + + async def run() -> None: + with pytest.raises(ValueError, match="凭证"): + await provider.async_list_files(extra_query={"access_token": "secret"}) + with pytest.raises(ValueError, match="凭证"): + await provider.async_create_context( + messages=[], extra_headers={"Authorization": "Bearer secret"} + ) + + asyncio.run(run()) + + +def test_anthropic_token_count_rejects_non_counting_fields(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider.count_tokens(CompletionRequest(prompt="hi", background=True)) + + +def test_anthropic_token_count_rejects_output_format_before_sdk_call(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class Messages: + def count_tokens(self, **kwargs): + calls.update(kwargs) + return SimpleNamespace(input_tokens=5) + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + with pytest.raises(ValueError, match="token count 不支持 output_format"): + provider.count_tokens(CompletionRequest(prompt="hi", output_format=dict)) + assert calls == {} + + async def run() -> None: + with pytest.raises(ValueError, match="token count 不支持 output_format"): + await provider.async_count_tokens( + CompletionRequest(prompt="hi", output_format=dict) + ) + + asyncio.run(run()) + + +def test_anthropic_token_count_forwards_supported_extra_body_sync_and_async(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class SyncMessages: + def count_tokens(self, **kwargs): + calls["sync"] = kwargs + return SimpleNamespace(input_tokens=5) + + class AsyncMessages: + async def count_tokens(self, **kwargs): + calls["async"] = kwargs + return SimpleNamespace(input_tokens=6) + + provider._get_client = lambda: SimpleNamespace( + messages=SyncMessages(), + beta=SimpleNamespace(messages=SyncMessages()), + ) + provider._get_aclient = lambda: SimpleNamespace( + messages=AsyncMessages(), + beta=SimpleNamespace(messages=AsyncMessages()), + ) + + request = CompletionRequest(prompt="hi", extra_body={"tenant": "demo"}) + assert provider.count_tokens(request) == 5 + assert provider.beta_count_tokens(request) == 5 + assert asyncio.run(provider.async_count_tokens(request)) == 6 + assert asyncio.run(provider.async_beta_count_tokens(request)) == 6 + assert calls["sync"]["extra_body"] == {"tenant": "demo"} + assert calls["async"]["extra_body"] == {"tenant": "demo"} + + +def test_anthropic_beta_facade_does_not_leak_stream_flag(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class Messages: + def create(self, **kwargs): + calls.update(kwargs) + return "beta" + + def count_tokens(self, **kwargs): + calls["count"] = kwargs + return SimpleNamespace(input_tokens=8) + + provider._get_client = lambda: SimpleNamespace(beta=SimpleNamespace(messages=Messages())) + assert provider.resources.beta_create(CompletionRequest(prompt="hi")) == "beta" + assert "stream" not in calls + assert provider.resources.beta_count_tokens(CompletionRequest(prompt="hi")) == 8 + + +def test_async_embedding_query_default_works_for_legacy_provider(): + class Embedding(TextEmbeddingModel): + def embed_documents(self, texts, **_kwargs): + return [[float(len(texts[0]))]] + + async def aembed_documents(self, texts, **_kwargs): + return [[float(len(texts[0]))]] + + assert asyncio.run(Embedding().aembed_query("abc")) == [3.0] diff --git a/tests/retrieval/test_parent_child_retrieval.py b/tests/retrieval/test_parent_child_retrieval.py index 899ef70..7652d89 100644 --- a/tests/retrieval/test_parent_child_retrieval.py +++ b/tests/retrieval/test_parent_child_retrieval.py @@ -1,6 +1,11 @@ +import asyncio +from types import SimpleNamespace from unittest.mock import MagicMock +import pytest + from src.retrieval.retriever import retrieve_documents +from src.services.retrieval_service import RetrievalService from src.utils.config import RetrievalMethod @@ -26,6 +31,213 @@ def resolve_parent_content(self, parent_id): return parent_document.get("content") +def test_hybrid_retrieval_starts_semantic_and_keyword_paths_in_parallel(): + service = object.__new__(RetrievalService) + started = [] + release = asyncio.Event() + + async def fake_semantic(_query, _top_k): + started.append("semantic") + await release.wait() + return ["semantic"] + + async def fake_keyword(_query, _top_k): + started.append("keyword") + await release.wait() + return ["keyword"] + + service._semantic_retrieve = fake_semantic + service._keyword_retrieve = fake_keyword + + async def run(): + task = asyncio.create_task(service._gather_hybrid_results("query", 3)) + async def wait_for_both_starts(): + while len(started) < 2: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_for_both_starts(), timeout=5) + release.set() + return await asyncio.wait_for(task, timeout=5) + + assert asyncio.run(run()) == (["semantic"], ["keyword"]) + assert set(started) == {"semantic", "keyword"} + + +def test_rerank_provider_is_cached_per_configuration(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + provider = MagicMock() + + async def fake_arerank(_query, _documents, top_n): + return [0], [float(top_n)] + + provider.arerank.side_effect = fake_arerank + factory = MagicMock(return_value=provider) + monkeypatch.setattr("src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", factory) + session_config = SimpleNamespace( + rerank_enabled=True, + active_rerank_configuration="siliconflow", + top_k=1, + rerank_configurations={"siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank")}, + ) + documents = [{"page_content": "doc", "score": 0.5, "metadata": {}}] + + async def run(): + first = await service._rerank_if_needed("query", documents, session_config) + second = await service._rerank_if_needed("query", documents, session_config) + return first, second + + first, second = asyncio.run(run()) + + assert first[0]["score"] == 1.0 + assert second[0]["score"] == 1.0 + factory.assert_called_once_with("siliconflow", session_config.rerank_configurations) + assert provider.arerank.call_count == 2 + + +def test_rerank_provider_cache_refreshes_when_options_change(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + first_provider = MagicMock() + second_provider = MagicMock() + factory = MagicMock(side_effect=[first_provider, second_provider]) + monkeypatch.setattr("src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", factory) + first_config = SimpleNamespace( + provider="siliconflow", + model_name="rerank", + options={"timeout": 10}, + ) + second_config = SimpleNamespace( + provider="siliconflow", + model_name="rerank", + options={"timeout": 20}, + ) + + assert service._get_rerank_provider("siliconflow", {"siliconflow": first_config}) is first_provider + assert service._get_rerank_provider("siliconflow", {"siliconflow": second_config}) is second_provider + assert factory.call_count == 2 + + +def test_retrieval_service_close_disposes_cached_rerank_and_embedding_models(): + rerank = MagicMock() + embedding = MagicMock() + service = RetrievalService(vector_store=object(), embedding_service=embedding) + service._rerank_providers = {("key", "provider", "model"): rerank} + + service.close() + + rerank.close.assert_called_once_with() + embedding.close.assert_called_once_with() + assert service._rerank_providers == {} + + +def test_retrieval_service_aclose_falls_back_to_sync_embedding_close(): + class SyncOnlyEmbedding: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + embedding = SyncOnlyEmbedding() + service = RetrievalService(vector_store=object(), embedding_service=embedding) + + asyncio.run(service.aclose()) + + assert embedding.closed + + +def test_rerank_top_n_is_clamped_after_candidate_reduction(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + provider = MagicMock() + + async def fake_arerank(_query, _documents, top_n): + assert top_n == 1 + return [0], [0.9] + + provider.arerank.side_effect = fake_arerank + monkeypatch.setattr( + "src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", + MagicMock(return_value=provider), + ) + session_config = SimpleNamespace( + rerank_enabled=True, + active_rerank_configuration="siliconflow", + top_k=5, + rerank_configurations={ + "siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank") + }, + ) + + result = asyncio.run( + service._rerank_if_needed( + "query", [{"page_content": "only", "score": 0.1, "metadata": {}}], session_config + ) + ) + + assert result[0]["score"] == 0.9 + + +def test_semantic_retrieve_uses_legacy_search_when_method_is_missing(): + class LegacyStore: + def search(self, query, top_k, search_type="semantic"): + return [{"page_content": query, "score": top_k, "metadata": {"search_type": search_type}}] + + class UnusedEmbedding: + async def embed_query(self, _query): + raise AssertionError("legacy semantic search should not require embeddings") + + embedding = UnusedEmbedding() + service = RetrievalService(LegacyStore(), embedding) + + result = asyncio.run(service._semantic_retrieve("query", 3)) + + assert result == [{"page_content": "query", "score": 3, "metadata": {"search_type": "semantic"}}] + + +def test_semantic_retrieve_does_not_hide_embedding_attribute_errors(): + class SemanticStore: + def semantic_search(self, _embedding, _top_k): + return [] + + class FailingEmbedding: + async def embed_query(self, _query): + raise AttributeError("embedding provider bug") + + service = RetrievalService(SemanticStore(), FailingEmbedding()) + + with pytest.raises(AttributeError, match="embedding provider bug"): + asyncio.run(service._semantic_retrieve("query", 3)) + + +def test_semantic_retrieve_does_not_hide_vector_store_attribute_errors(): + class BrokenStore: + def semantic_search(self, _embedding, _top_k): + raise AttributeError("vector store bug") + + class Embedding: + async def embed_query(self, _query): + return [0.1] + + service = RetrievalService(BrokenStore(), Embedding()) + + with pytest.raises(AttributeError, match="vector store bug"): + asyncio.run(service._semantic_retrieve("query", 3)) + + +@pytest.mark.parametrize( + ("indices", "scores", "message"), + [ + ([], [], "未返回可用结果"), + ([0], [], "数量不一致"), + ([2], [0.9], "index 无效"), + ([0, 0], [0.9, 0.8], "重复 index"), + ([0], [float("nan")], "必须是有限数值"), + ], +) +def test_rerank_output_contract_failures_are_explicit(indices, scores, message): + with pytest.raises(RuntimeError, match=message): + RetrievalService._validate_rerank_output(indices, scores, document_count=2) + + def test_retrieve_documents_uses_parent_sidecar_and_overfetches_candidates(monkeypatch): semantic_results = [ { @@ -101,7 +313,7 @@ def test_retrieve_documents_uses_parent_sidecar_and_overfetches_candidates(monke candidate_multiplier=3, ) - assert store.search_calls == [("semantic", 6), ("keyword", 6)] + assert sorted(store.search_calls) == [("keyword", 6), ("semantic", 6)] assert len(results) == 2 assert {doc["metadata"]["parent_id"] for doc in results} == {"parent-1", "parent-2"} assert all(doc["page_content"].startswith("parent content") for doc in results) diff --git a/tests/retrieval/vdb/test_factory.py b/tests/retrieval/vdb/test_factory.py index 6cc014b..ed712f5 100644 --- a/tests/retrieval/vdb/test_factory.py +++ b/tests/retrieval/vdb/test_factory.py @@ -1,12 +1,13 @@ -import pytest -from unittest.mock import MagicMock, patch -from typing import Any, List import sys +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest -from src.retrieval.vdb.factory import VectorStoreFactory from src.retrieval.vdb.base import VectorStoreBase -from src.utils.config import Settings, get_settings # 导入 Settings 和 get_settings -from src.retrieval.vdb.faiss_store import FaissStore # 导入 FaissStore +from src.retrieval.vdb.factory import VectorStoreFactory +from src.utils.config import Settings + # Mock FaissStore 类 class MockFaissStore(VectorStoreBase): @@ -15,25 +16,23 @@ def __init__(self, file_path: str | None): self.documents = [] self.embeddings = None - def add_documents(self, documents: List[dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): self.documents.extend(documents) - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: return [{"page_content": f"mock_doc_{i}", "metadata": {"source": "mock"}} for i in range(top_k)] - async def aadd_documents(self, documents: List[dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): self.documents.extend(documents) - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: return [{"page_content": f"mock_doc_{i}", "metadata": {"source": "mock"}} for i in range(top_k)] def save(self, path: str): """模拟保存操作""" - pass def load(self, path: str): """模拟加载操作""" - pass def get_embedding_model(self) -> Any: """模拟获取嵌入模型""" @@ -80,7 +79,9 @@ def patch_settings(monkeypatch, tmp_path): # 重新导入 VectorStoreFactory,确保它加载的是最新的版本 if 'src.retrieval.vdb.factory' in sys.modules: del sys.modules['src.retrieval.vdb.factory'] - from src.retrieval.vdb.factory import VectorStoreFactory as ReloadedVectorStoreFactory + from src.retrieval.vdb.factory import ( + VectorStoreFactory as ReloadedVectorStoreFactory, + ) # 直接模拟 VectorStoreFactory.get_vector_store 方法 def mock_get_vector_store(store_type: str, file_path: str | None = None) -> VectorStoreBase: diff --git a/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py b/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py index 67e1486..329108c 100644 --- a/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py +++ b/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py @@ -1,8 +1,18 @@ import numpy as np +import pytest from src.retrieval.vdb.faiss_store import FaissStore +def test_faiss_store_rejects_snapshot_without_embeddings_before_writing_npy(tmp_path): + store = FaissStore(file_path=None) + + with pytest.raises(RuntimeError, match="语义索引为空"): + store.save_snapshot(str(tmp_path / "snapshot")) + + assert not (tmp_path / "snapshot" / "embeddings.npy").exists() + + def test_faiss_store_persists_parent_sidecar_snapshot_roundtrip(tmp_path): snapshot_dir = tmp_path / "snapshot" store = FaissStore(file_path=None) @@ -34,3 +44,13 @@ def test_faiss_store_persists_parent_sidecar_snapshot_roundtrip(tmp_path): assert reloaded.resolve_parent_content("parent-1") == "parent content" assert reloaded.parent_documents["parent-1"]["metadata"]["source"] == "kb.md" assert reloaded.documents[0]["metadata"]["parent_id"] == "parent-1" + + +def test_faiss_store_rejects_query_dimension_mismatch(): + store = FaissStore(file_path=None) + store.documents = [{"page_content": "doc", "metadata": {}}] + store.embeddings = np.array([[0.0, 0.0]], dtype=np.float32) + store._rebuild_indices() + + with pytest.raises(ValueError, match="维度"): + store.semantic_search([0.0, 0.0, 0.0]) diff --git a/tests/retrieval/vdb/test_snapshot_repository.py b/tests/retrieval/vdb/test_snapshot_repository.py index c70d062..32cad76 100644 --- a/tests/retrieval/vdb/test_snapshot_repository.py +++ b/tests/retrieval/vdb/test_snapshot_repository.py @@ -39,3 +39,19 @@ def test_snapshot_repository_writes_and_loads_manifest(tmp_path, monkeypatch): assert loaded_manifest.snapshot_id == snapshot_id assert loaded_manifest.embedding_model == "local-hash-256" get_settings.cache_clear() + + +def test_snapshot_repository_cleans_only_requested_temp_dir(tmp_path): + run_config = type("RunConfig", (), {"snapshot_root": tmp_path})() + repository = SnapshotRepository(run_config) + snapshot_id = "kb-cleanup" + temp_dir = repository.create_temp_snapshot_dir(snapshot_id) + (temp_dir / "partial.bin").write_bytes(b"partial") + final_dir = tmp_path / "kb-existing" + final_dir.mkdir() + (final_dir / "keep.bin").write_bytes(b"keep") + + repository.cleanup_temp_snapshot_dir(snapshot_id) + + assert not temp_dir.exists() + assert (final_dir / "keep.bin").exists() diff --git a/tests/services/test_chat_service.py b/tests/services/test_chat_service.py new file mode 100644 index 0000000..5852c59 --- /dev/null +++ b/tests/services/test_chat_service.py @@ -0,0 +1,127 @@ +import asyncio + +import pytest + +from src.services.chat_service import ChatService + + +class _FailingModel: + async def ainvoke(self, **_kwargs): + raise AttributeError("provider response bug") + yield "" + + +class _EmptyModel: + async def ainvoke(self, **_kwargs): + if False: + yield "" + + +class _ModernAnthropicModel: + def __init__(self): + self.calls = [] + + def supports_sampling_option(self, name): + assert name == "temperature" + return False + + async def ainvoke(self, **kwargs): + self.calls.append(kwargs) + yield "检索" + + +class _ResponsesReasoningModel: + def __init__(self): + self.calls = [] + + async def ainvoke(self, **kwargs): + self.calls.append(kwargs) + yield "检索" + + +class _SamplingModel(_ResponsesReasoningModel): + def supports_sampling_option(self, name): + assert name == "temperature" + return True + + +def test_identify_intent_propagates_provider_failure(): + service = ChatService(_FailingModel(), retrieval_service=object()) + + with pytest.raises(RuntimeError, match="意图识别失败"): + asyncio.run(service.identify_intent("问题")) + + +def test_identify_intent_rejects_empty_provider_response(): + service = ChatService(_EmptyModel(), retrieval_service=object()) + + with pytest.raises(RuntimeError, match="返回空结果"): + asyncio.run(service.identify_intent("问题")) + + +def test_identify_intent_omits_sampling_for_models_that_reject_temperature(): + model = _ModernAnthropicModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert model.calls == [{"prompt": model.calls[0]["prompt"], "stream": False}] + + +def test_identify_intent_omits_sampling_without_capability_declaration(): + model = _ResponsesReasoningModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert "temperature" not in model.calls[0] + + +def test_generate_reply_omits_sampling_without_capability_declaration(): + model = _ResponsesReasoningModel() + service = ChatService(model, retrieval_service=object()) + + async def consume(): + return [ + chunk + async for chunk in service.generate_reply( + "问题", + "意图", + [], + type("SessionConfig", (), {"chat_temperature": 0.7})(), + ) + ] + + assert asyncio.run(consume()) == ["检索"] + assert "temperature" not in model.calls[0] + + +def test_chat_service_passes_sampling_when_capability_is_declared(): + model = _SamplingModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert model.calls[0]["temperature"] == 0.1 + + async def consume(): + return [ + chunk + async for chunk in service.generate_reply( + "问题", + "意图", + [], + type("SessionConfig", (), {"chat_temperature": 0.4})(), + ) + ] + + assert asyncio.run(consume()) == ["检索"] + assert model.calls[1]["temperature"] == 0.4 + + +def test_build_prompt_handles_documents_without_source_metadata(): + prompt = ChatService._build_prompt( + "问题", + "意图", + [{"page_content": "知识内容"}], + ) + + assert "来源: 未知来源" in prompt + assert "内容: 知识内容" in prompt diff --git a/tests/services/test_embedding_service.py b/tests/services/test_embedding_service.py new file mode 100644 index 0000000..4ecaf5b --- /dev/null +++ b/tests/services/test_embedding_service.py @@ -0,0 +1,181 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import numpy as np +import pytest + +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.services.embedding_service import EmbeddingService +from src.utils.config import ModelDetail + + +def test_embedding_service_passes_runtime_configurations_to_factory(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + factory = MagicMock(return_value=provider) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + factory, + ) + + service = EmbeddingService(run_config) + + assert service._get_model() is provider + factory.assert_called_once_with("custom", run_config.embedding_configurations) + + +def test_embedding_service_rejects_vectors_that_do_not_match_input_count(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + provider.aembed_documents = AsyncMock(return_value=[[1.0, 2.0]]) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + service = EmbeddingService(run_config) + + with pytest.raises(ValueError, match="向量数量与输入文本数量不一致"): + asyncio.run(service.embed_texts(["one", "two"])) + + +@pytest.mark.parametrize("value", [[], [[1.0, 2.0], [3.0, 4.0]], 1.0]) +def test_embedding_service_rejects_malformed_query_vectors(monkeypatch, value): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + provider.aembed_query = AsyncMock(return_value=value) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + with pytest.raises((TypeError, ValueError), match="向量|float"): + asyncio.run(EmbeddingService(run_config).embed_query("one")) + + +def test_embedding_service_does_not_replay_model_options(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail( + provider="openai", + model_name="text-embedding-3-small", + options={"dimensions": 512, "encoding_format": "float"}, + ) + }, + ) + provider = MagicMock() + provider.aembed_documents = AsyncMock(return_value=[[1.0, 2.0]]) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + service = EmbeddingService(run_config) + asyncio.run(service.embed_texts(["one"])) + + provider.aembed_documents.assert_awaited_once_with(["one"]) + + +@pytest.mark.parametrize("method", ["embed_texts", "embed_query", "embed_in_batches"]) +@pytest.mark.parametrize("options", [ + {"dimensions": 2}, + {"encoding_format": "float"}, + {"user": "embedding-test"}, + {"extra_body": {"vendor_flag": True}}, +]) +def test_embedding_service_model_options_reach_sdk_once(monkeypatch, method, options): + requests = [] + + class Embeddings: + async def create(self, **request): + requests.append(request) + return SimpleNamespace( + data=[SimpleNamespace(embedding=[1.0, 2.0]) for _ in request["input"]] + ) + + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._model_name = "text-embedding-3-small" + provider._options = options + provider._get_aclient = lambda: SimpleNamespace(embeddings=Embeddings()) + + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + service = EmbeddingService(SimpleNamespace( + default_embedding_provider="custom", kb_embedding_batch_size=1, + embedding_configurations={"custom": ModelDetail( + provider="openai", model_name="text-embedding-3-small", options=options, + )}, + )) + value = "one" if method == "embed_query" else ["one", "two"] + result = asyncio.run(getattr(service, method)(value)) + assert result.dtype == np.float32 + assert result.shape == ((2,) if method == "embed_query" else (2, 2)) + + assert len(requests) == (2 if method == "embed_in_batches" else 1) + for payload in requests: + for key, value in options.items(): + if key == "extra_body": + assert payload["extra_body"] == {"vendor_flag": True} + else: + assert payload[key] == value + + +def test_embedding_service_close_disposes_cached_model(): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + service = EmbeddingService(run_config) + service._embedding_model = provider + + service.close() + + provider.close.assert_called_once_with() + assert service._embedding_model is None + + +def test_embedding_service_aclose_disposes_cached_model_async(): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + closed = False + + async def aclose(): + nonlocal closed + closed = True + + provider.aclose = aclose + service = EmbeddingService(run_config) + service._embedding_model = provider + + asyncio.run(service.aclose()) + + assert closed + assert service._embedding_model is None diff --git a/tests/services/test_knowledge_build_service.py b/tests/services/test_knowledge_build_service.py new file mode 100644 index 0000000..6a74550 --- /dev/null +++ b/tests/services/test_knowledge_build_service.py @@ -0,0 +1,72 @@ +import asyncio +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from src.retrieval.snapshot_repository import SnapshotRepository +from src.services.knowledge_build_service import KnowledgeBuildService + + +class _RunConfig: + def __init__(self, knowledge_base_path: Path, snapshot_root: Path): + self.knowledge_base_path = knowledge_base_path + self.snapshot_root = snapshot_root + self.default_vector_store = "faiss" + + @property + def active_snapshot_marker(self) -> Path: + return self.snapshot_root / "ACTIVE_SNAPSHOT" + + +class _IncompleteVectorStore: + def save_snapshot(self, snapshot_dir: str) -> None: + (Path(snapshot_dir) / "chunks.pkl").write_bytes(b"partial") + + def upsert_embeddings(self, documents, embeddings) -> None: + raise AssertionError("空文档不应写入 embedding") + + +class _EmptyPipeline: + def process(self, document): + return [] + + +def test_build_validates_temp_snapshot_before_activation(tmp_path, monkeypatch): + knowledge_base = tmp_path / "knowledge_base" + knowledge_base.mkdir() + (knowledge_base / "article.md").write_text("内容", encoding="utf-8") + + snapshot_root = tmp_path / "snapshots" + snapshot_root.mkdir() + previous_snapshot = snapshot_root / "previous" + previous_snapshot.mkdir() + (previous_snapshot / "keep.txt").write_text("保留", encoding="utf-8") + (snapshot_root / "ACTIVE_SNAPSHOT").write_text("previous", encoding="utf-8") + + run_config = _RunConfig(knowledge_base, snapshot_root) + repository = SnapshotRepository(run_config) + monkeypatch.setattr(repository, "generate_snapshot_id", lambda: "kb-new") + monkeypatch.setattr( + "src.services.knowledge_build_service.Pipeline.from_file_path", + lambda *args, **kwargs: _EmptyPipeline(), + ) + + embedding_service = SimpleNamespace( + embedding_provider_key="local-hash", + embedding_model_detail=SimpleNamespace(model_name="local-hash-32"), + ) + service = KnowledgeBuildService( + run_config, + _IncompleteVectorStore(), + embedding_service, + repository, + ) + + with pytest.raises(FileNotFoundError, match="知识快照不完整"): + asyncio.run(service.build("standard")) + + assert (snapshot_root / "ACTIVE_SNAPSHOT").read_text(encoding="utf-8") == "previous" + assert (previous_snapshot / "keep.txt").read_text(encoding="utf-8") == "保留" + assert not (snapshot_root / "kb-new").exists() + assert not (snapshot_root / ".tmp-kb-new").exists() diff --git a/tests/test_config.py b/tests/test_config.py index bfdbef0..588f43a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,11 +1,16 @@ -# -*- coding: utf-8 -*- import os import pytest from pydantic import ValidationError -from src.utils.config import RetrievalMethod, Settings, get_settings, resolve_app_root - +from src.utils.config import ( + ModelDetail, + ModelProtocol, + RetrievalMethod, + Settings, + get_settings, + resolve_app_root, +) MOCK_TOML_CONTENT = """ log_level = "INFO" @@ -16,6 +21,10 @@ retrieval_candidate_multiplier = 4 kb_child_chunk_size = 180 kb_child_chunk_overlap = 18 +google_genai_use_vertexai = true +gemini_api_key = "toml-gemini-key" +google_cloud_project = "toml-project" +google_cloud_location = "europe-west4" [embedding_configurations.google] provider = "google" @@ -24,6 +33,8 @@ [llm_configurations.demo] provider = "openai" model_name = "demo-model" +protocol = "responses" +options = { top_p = 0.8, response_format = { type = "json_object" } } """ @@ -59,6 +70,12 @@ def test_settings_model_validation(): with pytest.raises(ValidationError): Settings(chat_retrieval_method="UNKNOWN_METHOD") # type: ignore[arg-type] + with pytest.raises(ValidationError, match="chat_top_k"): + Settings(chat_top_k=0) + + with pytest.raises(ValidationError, match="chat_score_threshold"): + Settings(chat_score_threshold=1.1) + def test_settings_splitter_separators_empty_string_falls_back_to_default(): settings = Settings(kb_splitter_separators="") @@ -84,7 +101,26 @@ def test_settings_defaults(): assert settings.kb_child_chunk_overlap == 30 assert settings.default_embedding_provider == "local-hash" assert settings.embedding_configurations["local-hash"].model_name == "local-hash-256" - assert settings.llm_configurations["iflow-qwen3-max"].model_name == "qwen3-max" + assert settings.llm_configurations["openai"].model_name == "gpt-4o" + assert settings.qwen_base_url.endswith("/compatible-mode/v1") + assert settings.volc_base_url.endswith("/api/v3") + assert "jina" not in settings.embedding_configurations + + +def test_settings_dump_hides_credentials_by_default(): + settings = Settings(openai_api_key="sk-test", lm_studio_api_key="local-secret") + + assert "openai_api_key" not in settings.model_dump() + assert "lm_studio_api_key" not in settings.model_dump_json() + assert settings.model_dump(include_secrets=True)["openai_api_key"] == "sk-test" + + +def test_settings_supports_gemini_api_key_alias_without_exposing_it_by_default(): + settings = Settings(gemini_api_key="gemini-test") + + assert settings.gemini_api_key == "gemini-test" + assert "gemini_api_key" not in settings.model_dump() + assert settings.model_dump(include_secrets=True)["gemini_api_key"] == "gemini-test" def test_settings_from_toml(tmp_path): @@ -101,19 +137,100 @@ def test_settings_from_toml(tmp_path): assert settings.retrieval_candidate_multiplier == 4 assert settings.kb_child_chunk_size == 180 assert settings.kb_child_chunk_overlap == 18 + assert settings.google_genai_use_vertexai is True + assert settings.gemini_api_key == "toml-gemini-key" + assert settings.google_cloud_project == "toml-project" + assert settings.google_cloud_location == "europe-west4" assert settings.embedding_configurations["google"].model_name == "toml-embedding-model" assert settings.llm_configurations["demo"].provider == "openai" assert settings.llm_configurations["demo"].model_name == "demo-model" + assert settings.llm_configurations["demo"].protocol == ModelProtocol.RESPONSES + assert settings.llm_configurations["demo"].options["top_p"] == 0.8 + + +def test_model_protocol_aliases_and_invalid_values(): + assert ModelDetail(provider="openai", model_name="gpt-test", protocol="response").protocol == ModelProtocol.RESPONSES + + with pytest.raises(ValidationError, match="不支持的模型协议"): + ModelDetail(provider="openai", model_name="gpt-test", protocol="unknown") + + +@pytest.mark.parametrize("field_name", ["embedding_configurations", "rerank_configurations"]) +def test_settings_rejects_protocol_on_non_llm_configuration(field_name): + with pytest.raises(ValidationError, match="配置不支持 protocol"): + Settings( + **{ + field_name: { + "demo": { + "provider": "openai", + "model_name": "demo-model", + "protocol": "responses", + } + } + } + ) + + +@pytest.mark.parametrize("sensitive_key", [ + "X-API-Key", "X-Auth-Token", "Authorization", "access_token", "api_key", "Bearer", +]) +def test_model_options_reject_credentials_nested_in_headers_or_query(sensitive_key): + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={"default_headers": {sensitive_key: "secret"}}, + ) + + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={"default_query": {sensitive_key: "secret"}}, + ) + + +@pytest.mark.parametrize("container", [ + "headers", "default_headers", "extra_headers", + "query", "default_query", "extra_query", "http_options", "header", "params", + "http_client", "httpx_client", "httpx_async_client", "aiohttp_client", +]) +def test_model_options_reject_request_header_and_query_containers(container): + with pytest.raises(ValueError, match="凭证或连接字段"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={container: {"X-Trace-ID": "trace"}}, + ) + + +@pytest.mark.parametrize("connection_key", ["base_url", "client_args", "async_client_args"]) +def test_model_options_reject_connection_overrides(connection_key): + with pytest.raises(ValueError, match="凭证或连接字段"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={connection_key: "https://proxy.invalid"}, + ) def test_settings_from_dotenv(tmp_path): dotenv_path = tmp_path / ".env" - dotenv_path.write_text('OPENAI_API_KEY="dotenv_key"\nCHAT_TOP_K=15', encoding="utf-8") + dotenv_path.write_text( + 'OPENAI_API_KEY="dotenv_key"\nCHAT_TOP_K=15\n' + 'GOOGLE_GENAI_USE_VERTEXAI=true\n' + 'GOOGLE_CLOUD_PROJECT="dotenv-project"\n' + 'GOOGLE_CLOUD_LOCATION="asia-east1"\n', + encoding="utf-8", + ) settings = get_settings() assert settings.openai_api_key == "dotenv_key" assert settings.chat_top_k == 15 + assert settings.google_genai_use_vertexai is True + assert settings.google_cloud_project == "dotenv-project" + assert settings.google_cloud_location == "asia-east1" def test_settings_from_env_vars(monkeypatch): diff --git a/tests/test_main_entry.py b/tests/test_main_entry.py index ca2d0aa..0411d9d 100644 --- a/tests/test_main_entry.py +++ b/tests/test_main_entry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import main diff --git a/tests/test_release_scripts.py b/tests/test_release_scripts.py index 3d7deb8..d23d96d 100644 --- a/tests/test_release_scripts.py +++ b/tests/test_release_scripts.py @@ -1,8 +1,12 @@ -# -*- coding: utf-8 -*- import subprocess -from pathlib import Path -from scripts.build_binary_release import prepare_runtime_layout, validate_bundle +import pytest + +from scripts.build_binary_release import ( + prepare_runtime_layout, + validate_bundle, + validate_target_environment, +) from scripts.extract_release_notes import extract_section @@ -17,10 +21,10 @@ def test_prepare_runtime_layout_creates_placeholder_directories(tmp_path): def test_extract_section_returns_version_content(): - section = extract_section("1.3.0") + section = extract_section("1.4.0") - assert section.startswith("## [1.3.0]") - assert "运行与配置" in section + assert section.startswith("## [1.4.0]") + assert "Provider 与协议" in section def test_validate_bundle_uses_smoke_test(monkeypatch, tmp_path): @@ -35,7 +39,7 @@ def test_validate_bundle_uses_smoke_test(monkeypatch, tmp_path): def fake_run(command, **kwargs): recorded["command"] = command recorded["kwargs"] = kwargs - return subprocess.CompletedProcess(command, 0, stdout="PyRAG-Kit 1.3.0 smoke test ok\n", stderr="") + return subprocess.CompletedProcess(command, 0, stdout="PyRAG-Kit 1.4.0 smoke test ok\n", stderr="") monkeypatch.setattr("scripts.build_binary_release.subprocess.run", fake_run) @@ -43,3 +47,22 @@ def fake_run(command, **kwargs): assert recorded["command"][-1] == "--smoke-test" assert recorded["kwargs"]["check"] is True + + +@pytest.mark.parametrize( + ("target", "system", "machine"), + [ + ("macos-x64", "Darwin", "x86_64"), + ("macos-arm64", "Darwin", "arm64"), + ("linux-x64", "Linux", "amd64"), + ("linux-arm64", "Linux", "aarch64"), + ("windows-x64", "Windows", "AMD64"), + ], +) +def test_validate_target_environment_accepts_matching_host(target, system, machine): + validate_target_environment(target, system=system, machine=machine) + + +def test_validate_target_environment_rejects_cross_architecture(): + with pytest.raises(RuntimeError, match="不执行跨平台交叉编译"): + validate_target_environment("macos-arm64", system="Darwin", machine="x86_64") From 28afad05a888e74235acd4082c83742d9cf15d8d Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 11:33:45 +0800 Subject: [PATCH 10/35] docs: clarify Ark protocol alias in changelog Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 524f668..2e74a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - 建立统一 Provider 工厂与抽象请求模型,补齐 LLM、Embedding、Rerank 的能力声明、配置校验、缓存与同步/异步生命周期管理。 - 新增 `complete()`/`acomplete()` 高级调用入口,统一支持多轮消息、工具调用、结构化输出、多模态内容、usage 与结束原因;`invoke()`/`ainvoke()` 保留文本流兼容行为。 -- 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses。 +- 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类线协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses,Ark 的 `ark` 取值是 Chat Completions 的别名。 - Responses 对非官方端点要求 `options.server_verified_protocols = ["responses"]` 显式登记;未登记时工厂、Provider 和资源 Facade 均在请求前拒绝,避免把本地 SDK 资源误发到未实现 `/responses` 的服务端。 - 新增原生资源 Facade,显式暴露文件、批处理、缓存、向量库、token 计数、调优等 SDK 能力,不再把资源生命周期混入普通聊天请求。 - 深度适配 Google GenAI、Anthropic、Volcengine Ark、Jina 与 SiliconFlow Rerank;OpenAI 兼容渠道复用 `openai` SDK,Jina 与 SiliconFlow Rerank 使用 HTTP JSON。 From 00744b824c7e0f1824a0a4b2c6157fe9e672ac5f Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 15:16:43 +0800 Subject: [PATCH 11/35] fix: normalize flat tool_calls for chat completions history CompletionResult.tool_calls uses the flat shape {"id","type","name","arguments"}, while OpenAI Chat Completions requires assistant history tool_calls in the nested shape {"id","type","function":{"name","arguments"}}. Replaying our own output into a follow-up request therefore sent a malformed payload and the server rejected the turn with HTTP 400, breaking multi-turn tool calls for every OpenAI-compatible channel. Normalize both shapes at the request boundary so callers can feed the previous turn's tool_calls back verbatim. Anthropic, Google and Ark already read the nested form via a flat fallback, so they keep working; the normalization also fixes the Ark Chat path, which shares normalize_messages. Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/__base__/model_provider.py | 74 +++++++++++++++++++++-- tests/providers/test_sdk_capabilities.py | 76 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 6a5a291..68bccae 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -324,6 +324,74 @@ def merge_tool_call_fragment( return key, merged +def _chat_tool_call_item( + tool_call: Mapping[str, Any], + location: str, +) -> dict[str, Any]: + """将工具调用归一化为 OpenAI Chat Completions 的嵌套结构。 + + ``CompletionResult.tool_calls`` 使用扁平的 ``{"id", "type", "name", + "arguments"}`` 形状,而 Chat Completions 要求 assistant 历史的 + ``tool_calls`` 使用 ``{"id", "type", "function": {"name", "arguments"}}``。 + 两种输入都在这里统一,调用方可以把上一轮的 ``tool_calls`` 原样回填。 + """ + if not isinstance(tool_call, Mapping): + raise ValueError(f"{location} 必须是对象。") + normalized = dict(tool_call) + + function = normalized.get("function") + if function is not None and not isinstance(function, Mapping): + raise ValueError(f"{location}.function 必须是对象。") + function_values = function if isinstance(function, Mapping) else normalized + + name = function_values.get("name") or normalized.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{location} 缺少函数名。") + arguments = function_values.get("arguments") + if arguments is None: + arguments = normalized.get("arguments") + # 复用统一助手:非字符串参数按 JSON 文本发送,None 保持空串, + # 与 Responses 适配和流式工具调用使用同一套规则。 + arguments = normalize_tool_arguments(arguments) + + call_id = normalized.get("id") or normalized.get("call_id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError(f"{location} 缺少 id/call_id。") + + item: dict[str, Any] = { + "id": call_id, + "type": normalized.get("type") or "function", + "function": {"name": name, "arguments": arguments}, + } + return item + + +def normalize_chat_messages( + messages: Sequence[Mapping[str, Any]], + location: str = "messages", +) -> list[dict[str, Any]]: + """归一化 Chat Completions 消息,转换工具调用并保留其它字段。""" + normalized: list[dict[str, Any]] = [] + for index, message in enumerate(messages): + if not isinstance(message, Mapping): + raise ValueError(f"{location}[{index}] 必须是对象。") + item = dict(message) + tool_calls = item.get("tool_calls") + if tool_calls is not None: + if isinstance(tool_calls, (str, bytes, bytearray)) or not isinstance( + tool_calls, Sequence + ): + raise ValueError(f"{location}[{index}].tool_calls 必须是序列。") + item["tool_calls"] = [ + _chat_tool_call_item( + tool_call, f"{location}[{index}].tool_calls[{call_index}]" + ) + for call_index, tool_call in enumerate(tool_calls) + ] + normalized.append(item) + return normalized + + def normalize_messages( prompt: str | None, system_prompt: str | None, @@ -333,11 +401,7 @@ def normalize_messages( if messages is not None: if isinstance(messages, (str, bytes, bytearray)) or not isinstance(messages, Sequence): raise ValueError("messages 必须是消息对象序列。") - normalized: list[dict[str, Any]] = [] - for index, message in enumerate(messages): - if not isinstance(message, Mapping): - raise ValueError(f"messages[{index}] 必须是对象。") - normalized.append(dict(message)) + normalized = normalize_chat_messages(messages) if system_prompt and not any(message.get("role") == "system" for message in normalized): normalized.insert(0, {"role": "system", "content": system_prompt}) return normalized diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py index e26c4e2..ed9f5a0 100644 --- a/tests/providers/test_sdk_capabilities.py +++ b/tests/providers/test_sdk_capabilities.py @@ -5545,3 +5545,79 @@ async def aembed_documents(self, texts, **_kwargs): return [[float(len(texts[0]))]] assert asyncio.run(Embedding().aembed_query("abc")) == [3.0] + + +def test_chat_completions_normalizes_flat_tool_calls_to_openai_shape(): + """我们产出的扁平 tool_calls 必须能在回填时转换成 OpenAI 规范结构。 + + ``CompletionResult.tool_calls`` 使用扁平的 ``{"id","type","name", + "arguments"}`` 形状;OpenAI Chat Completions 要求 assistant 历史的 + ``tool_calls`` 使用嵌套的 ``{"id","type","function":{"name", + "arguments"}}``。适配器必须在请求边界完成转换,否则多轮工具调用会 + 被服务端以 400 拒绝。 + """ + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + flat = {"id": "call-1", "type": "function", "name": "lookup", + "arguments": '{"id":1}'} + request = provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", "tool_calls": [flat]}, + {"role": "tool", "tool_call_id": "call-1", "content": "结果"}, + ], + stream=False, + ) + + tool_calls = request["messages"][1]["tool_calls"] + assert tool_calls == [{ + "id": "call-1", + "type": "function", + "function": {"name": "lookup", "arguments": '{"id":1}'}, + }], "扁平 tool_calls 必须转换为 OpenAI 嵌套结构" + + +def test_chat_completions_preserves_native_tool_calls(): + """已经是 OpenAI 原生嵌套结构的历史必须原样保留。""" + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + native = {"id": "call-2", "type": "function", + "function": {"name": "lookup", "arguments": '{"id":2}'}} + request = provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", "tool_calls": [native]}, + {"role": "tool", "tool_call_id": "call-2", "content": "结果"}, + ], + stream=False, + ) + + assert request["messages"][1]["tool_calls"] == [native] + + +def test_chat_completions_rejects_tool_call_without_function_name(): + """缺少函数名的工具调用必须显式报错,不能静默发出无效请求。""" + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="函数名"): + provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", + "tool_calls": [{"id": "call-3", "type": "function", + "arguments": "{}"}]}, + ], + stream=False, + ) From 0b3d594841e5f18532b6ed965fc733b03fff0984 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 20:58:25 +0800 Subject: [PATCH 12/35] fix: stop forcing tool_call ids in shared message normalization 153bb07 made normalize_messages require an id/call_id on every assistant tool_call. That helper runs for every provider, but Gemini's FunctionCall.id is optional and the SDK leaves it None, so replaying a Google tool call into the next turn was rejected locally before the request ever reached the SDK. Google's own _contents accepts id-less calls, so the regression lived purely in the normalization layer. Keep the public helper shape-only: it preserves an id when present and drops the field otherwise. Protocols that genuinely need an id (Chat Completions, Responses) now pass require_tool_call_ids=True explicitly, so their stricter validation is unchanged. Also fix two sibling gaps in the same helper: read the ``input`` alias used by Responses custom_tool_call and Anthropic tool_use instead of silently normalizing arguments to an empty string, and map the function_call/custom_tool_call type values back to ``function``, which is the only value Chat Completions accepts. Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/__base__/model_provider.py | 58 +++++++-- src/providers/openai_compatible.py | 9 +- src/providers/volcengine.py | 12 +- tests/providers/test_sdk_capabilities.py | 152 +++++++++++++++++++++++ 4 files changed, 222 insertions(+), 9 deletions(-) diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 68bccae..50ed5fd 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -324,9 +324,14 @@ def merge_tool_call_fragment( return key, merged +_CHAT_TOOL_CALL_TYPES = frozenset({"function"}) + + def _chat_tool_call_item( tool_call: Mapping[str, Any], location: str, + *, + require_call_id: bool = False, ) -> dict[str, Any]: """将工具调用归一化为 OpenAI Chat Completions 的嵌套结构。 @@ -334,6 +339,11 @@ def _chat_tool_call_item( "arguments"}`` 形状,而 Chat Completions 要求 assistant 历史的 ``tool_calls`` 使用 ``{"id", "type", "function": {"name", "arguments"}}``。 两种输入都在这里统一,调用方可以把上一轮的 ``tool_calls`` 原样回填。 + + 历史工具调用并不总是带 ``id``:Gemini 的 ``FunctionCall.id`` 是可选字段, + SDK 默认 ``None``,其 ``_contents`` 也允许无 id 的调用。因此默认只在调用方 + 确实提供 id 时保留该字段,不用本地错误打断本可发送的请求;只有确实要求 + ``id`` 的协议(Chat Completions)才传入 ``require_call_id=True`` 显式校验。 """ if not isinstance(tool_call, Mapping): raise ValueError(f"{location} 必须是对象。") @@ -350,25 +360,40 @@ def _chat_tool_call_item( arguments = function_values.get("arguments") if arguments is None: arguments = normalized.get("arguments") + if arguments is None: + # ``input`` 是 Responses ``custom_tool_call`` 与 Anthropic ``tool_use`` + # 的参数别名,回退顺序与 Responses 适配保持一致。 + arguments = normalized.get("input") # 复用统一助手:非字符串参数按 JSON 文本发送,None 保持空串, # 与 Responses 适配和流式工具调用使用同一套规则。 arguments = normalize_tool_arguments(arguments) call_id = normalized.get("id") or normalized.get("call_id") if not isinstance(call_id, str) or not call_id.strip(): - raise ValueError(f"{location} 缺少 id/call_id。") + if require_call_id: + raise ValueError(f"{location} 缺少 id/call_id。") + call_id = None + + # Chat Completions 的 ``tool_calls[].type`` 只接受 ``function``;Responses 的 + # ``function_call``/``custom_tool_call`` 与流式片段都要映射回该取值。 + tool_type = normalized.get("type") + if not isinstance(tool_type, str) or tool_type not in _CHAT_TOOL_CALL_TYPES: + tool_type = "function" item: dict[str, Any] = { - "id": call_id, - "type": normalized.get("type") or "function", + "type": tool_type, "function": {"name": name, "arguments": arguments}, } + if call_id is not None: + item["id"] = call_id return item def normalize_chat_messages( messages: Sequence[Mapping[str, Any]], location: str = "messages", + *, + require_tool_call_ids: bool = False, ) -> list[dict[str, Any]]: """归一化 Chat Completions 消息,转换工具调用并保留其它字段。""" normalized: list[dict[str, Any]] = [] @@ -384,7 +409,9 @@ def normalize_chat_messages( raise ValueError(f"{location}[{index}].tool_calls 必须是序列。") item["tool_calls"] = [ _chat_tool_call_item( - tool_call, f"{location}[{index}].tool_calls[{call_index}]" + tool_call, + f"{location}[{index}].tool_calls[{call_index}]", + require_call_id=require_tool_call_ids, ) for call_index, tool_call in enumerate(tool_calls) ] @@ -396,12 +423,21 @@ def normalize_messages( prompt: str | None, system_prompt: str | None, messages: Sequence[Mapping[str, Any]] | None, + *, + require_tool_call_ids: bool = False, ) -> list[dict[str, Any]]: - """生成不修改调用方对象的标准消息列表。""" + """生成不修改调用方对象的标准消息列表。 + + ``require_tool_call_ids`` 只应由确实要求 assistant 历史工具调用带 ``id`` + 的协议(Chat Completions)开启;其它协议继续接受 Gemini 这类无 id 的历史。 + """ if messages is not None: if isinstance(messages, (str, bytes, bytearray)) or not isinstance(messages, Sequence): raise ValueError("messages 必须是消息对象序列。") - normalized = normalize_chat_messages(messages) + normalized = normalize_chat_messages( + messages, + require_tool_call_ids=require_tool_call_ids, + ) if system_prompt and not any(message.get("role") == "system" for message in normalized): normalized.insert(0, {"role": "system", "content": system_prompt}) return normalized @@ -837,8 +873,16 @@ def normalize_responses_input( 输入消息类型,必须分别变成 function_call 和 function_call_output。 原生 Responses item 会保留,但其关键字段仍在边界处校验。``provider`` 为 ``ark``/``volcengine`` 时启用 Ark 独有的音视频内容块。 + + Responses 输入项要求每个 ``function_call`` 带 ``call_id``,因此这里强制 + assistant 历史的工具调用提供 id;缺少关联 ID 会在请求前显式报错。 """ - normalized = normalize_messages(prompt, system_prompt, messages) + normalized = normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + ) if messages is None and prompt is not None: return prompt diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py index a4da675..e293ebd 100644 --- a/src/providers/openai_compatible.py +++ b/src/providers/openai_compatible.py @@ -780,7 +780,14 @@ def _build_chat_request( extra_query, f"{getattr(self, '_provider', 'openai')} Chat Completions", ) - normalized_messages = normalize_messages(prompt, system_prompt, messages) + # Chat Completions 要求 assistant 历史的每个 tool_call 都带 id; + # 这里显式校验,其它协议继续接受 Gemini 这类无 id 的历史。 + normalized_messages = normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + ) request: dict[str, Any] = { "model": self._model_name, diff --git a/src/providers/volcengine.py b/src/providers/volcengine.py index d6d53c1..a5a6520 100644 --- a/src/providers/volcengine.py +++ b/src/providers/volcengine.py @@ -582,7 +582,17 @@ def _build_chat_request(self, prompt: str | None = None, system_prompt: str | No extra_query, "Ark Chat Completions", ) - request: dict[str, Any] = {"model": self._model_name, "messages": normalize_messages(prompt, system_prompt, messages), "stream": stream} + request: dict[str, Any] = { + "model": self._model_name, + # Ark Chat Completions 同样要求 assistant 历史的 tool_call 带 id。 + "messages": normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + ), + "stream": stream, + } configured_options = self._request_options() self._validate_model_options( configured_options, diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py index ed9f5a0..2cdf2f5 100644 --- a/tests/providers/test_sdk_capabilities.py +++ b/tests/providers/test_sdk_capabilities.py @@ -11,6 +11,9 @@ CompletionRequest, LargeLanguageModel, TextEmbeddingModel, + normalize_chat_messages, + normalize_messages, + normalize_responses_input, ) from src.providers.anthropic import AnthropicProvider from src.providers.factory import ModelProviderFactory @@ -5621,3 +5624,152 @@ def test_chat_completions_rejects_tool_call_without_function_name(): ], stream=False, ) + + +def test_normalize_messages_accepts_id_less_tool_calls_for_non_chat_protocols(): + """Google 这类协议必须接受无 id 的历史工具调用。 + + Gemini 的 ``FunctionCall.id`` 是可选字段且 SDK 默认 ``None``,其 + ``_contents`` 明确允许无 id 的调用。公共的 ``normalize_messages`` 因此不能 + 强制要求 id,否则多轮工具调用会在到达 SDK 之前就被本地拒绝。 + """ + normalized = normalize_messages( + None, + None, + [ + {"role": "user", "content": "北京天气"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "get_weather", + "arguments": {"city": "BJ"}} + ], + }, + {"role": "tool", "tool_call_id": "get_weather", + "name": "get_weather", "content": '{"t":20}'}, + ], + ) + + tool_call = normalized[1]["tool_calls"][0] + assert "id" not in tool_call + assert tool_call["type"] == "function" + assert tool_call["function"] == { + "name": "get_weather", + "arguments": '{"city":"BJ"}', + } + + +def test_google_multi_turn_replay_accepts_id_less_tool_calls(): + """把上一轮无 id 的工具调用原样回填给 Google 必须成功。""" + contents = GoogleProvider._contents( + normalize_messages( + None, + None, + [ + {"role": "user", "content": "北京天气"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "get_weather", + "arguments": {"city": "BJ"}} + ], + }, + {"role": "tool", "tool_call_id": "get_weather", + "name": "get_weather", "content": '{"t":20}'}, + ], + ), + None, + None, + ) + + function_call = next( + part.function_call + for content in contents + for part in content.parts + if part.function_call + ) + assert function_call.name == "get_weather" + + +def test_chat_tool_call_normalization_reads_input_alias(): + """Responses/Anthropic 用 ``input`` 承载参数,回填时不能静默变空。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "c1", "type": "custom_tool_call", "name": "n", + "input": '{"a":1}'} + ], + } + ] + ) + + assert normalized[0]["tool_calls"] == [ + {"id": "c1", "type": "function", + "function": {"name": "n", "arguments": '{"a":1}'}} + ] + + +@pytest.mark.parametrize( + "tool_type", + ["function", "function_call", "custom_tool_call", "tool_call"], +) +def test_chat_tool_call_normalization_maps_type_to_function(tool_type): + """Chat Completions 只接受 ``type="function"``,其它来源取值都要映射。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "x", "type": tool_type, "name": "n", "arguments": "{}"} + ], + } + ] + ) + + assert normalized[0]["tool_calls"][0]["type"] == "function" + + +def test_chat_completions_still_requires_tool_call_id(): + """Chat Completions 的严格校验不能被放宽,缺 id 必须显式报错。""" + with pytest.raises(ValueError, match="缺少 id/call_id"): + normalize_messages( + None, + None, + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "n", + "arguments": "{}"} + ], + } + ], + require_tool_call_ids=True, + ) + + +def test_responses_input_still_requires_tool_call_id(): + """Responses 输入项需要 call_id,缺 id 仍要在请求前显式报错。""" + with pytest.raises(ValueError, match="缺少 id/call_id"): + normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "n", + "arguments": "{}"} + ], + } + ], + ) + From 191836e2fa40f8a71228bb2d19f0fb981018a622 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 20:58:36 +0800 Subject: [PATCH 13/35] fix: cover short credential keys and scrub the cached traceback Two gaps in the credential boundary: ``ak`` and ``sk`` are the key names VolcengineProvider uses to send its own VOLC_ACCESS_KEY/VOLC_SECRET_KEY, and ``account_key`` is Azure Storage's, but none were recognized as credential keys, so a request override carrying them passed validation while ``api_key`` was rejected. Add them with an exact normalized-key match; ordinary fields such as task, risk, ask, disk and sketch do not collide. ``RedactingFormatter`` formatted first and redacted second, but ``logging.Formatter.format`` caches the formatted traceback on ``record.exc_text`` for later handlers to reuse. A non-redacting handler on the same logger therefore still emitted the raw credential. Clear the cache before formatting and write the redacted text back, so what later handlers reuse is already scrubbed. This covers handlers that run after ours; one ordered before ours has already emitted. Also widen redaction to ``ak=``/``sk=``/``account_key=`` and the URL query form, using a word-boundary guard so ordinary text is untouched. Co-Authored-By: Claude Opus 5 (1M context) --- src/utils/log_manager.py | 22 ++++++++- src/utils/security.py | 20 +++++++- tests/chat/test_core.py | 31 ++++++++++++ tests/providers/test_security_boundaries.py | 52 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 tests/providers/test_security_boundaries.py diff --git a/src/utils/log_manager.py b/src/utils/log_manager.py index 831ffd7..c28450b 100644 --- a/src/utils/log_manager.py +++ b/src/utils/log_manager.py @@ -9,10 +9,28 @@ class RedactingFormatter(logging.Formatter): - """在格式化完整 traceback 后脱敏,避免异常链泄漏凭证。""" + """在格式化完整 traceback 后脱敏,避免异常链泄漏凭证。 + + ``logging.Formatter.format`` 会把格式化后的 traceback 缓存在 + ``record.exc_text`` 上,后续 handler 直接复用该缓存。如果本 formatter 只是 + 在 ``super().format()`` 之后脱敏,缓存里留下的仍是未脱敏文本,随后执行的 + 非脱敏 handler(例如宿主应用自行挂在 root logger 上的 handler)就会拿到原始 + 凭证。因此这里在格式化前清空缓存、在脱敏后写回,使缓存内容本身也是脱敏的。 + + 边界:Python logging 的 handler 顺序决定,只有在脱敏 handler 先于其它 + handler 执行时才能保护它们。项目自身的 logger 满足该前提(子 logger 的 + handler 先执行,再传播到 root);若宿主在同一个 logger 上把普通 handler + 排在脱敏 handler 之前,那个 handler 已先输出了原始内容,无法追回。 + """ def format(self, record: logging.LogRecord) -> str: - return redact_sensitive_text(super().format(record)) + record.exc_text = None + formatted = super().format(record) + # Formatter 会把 traceback 写回 ``record.exc_text``;用脱敏结果覆盖, + # 让后续 handler 复用到的缓存也是脱敏后的文本。 + if record.exc_text is not None: + record.exc_text = redact_sensitive_text(record.exc_text) + return redact_sensitive_text(formatted) def get_chat_logger() -> logging.Logger: """ diff --git a/src/utils/security.py b/src/utils/security.py index a7606c3..e3a1937 100644 --- a/src/utils/security.py +++ b/src/utils/security.py @@ -34,6 +34,14 @@ "xapikey", "accesskey", "secretkey", + # 火山引擎 SDK 的客户端凭证键名(``ak``/``sk``);本项目自己就用它们 + # 承载 VOLC_ACCESS_KEY / VOLC_SECRET_KEY,因此必须和 ``api_key`` 一样 + # 被识别为凭证。按规范化后的完整键名精确匹配,``task``/``risk``/``ask`` + # 这类普通字段不会命中。 + "ak", + "sk", + # Azure 存储的 ``account_key`` 也是凭证键名。 + "accountkey", "token", "password", "passwd", @@ -96,9 +104,18 @@ ) _URL_USERINFO_RE = re.compile(r"(?i)(https?://)([^\s/@:]+):([^\s/@]+)@") _URL_QUERY_SECRET_RE = re.compile( - r"(?i)([?&](?:api[_-]?key|access[_-]?key|secret[_-]?key|token|password|authorization)=)" + r"(?i)([?&](?:api[_-]?key|access[_-]?key|secret[_-]?key|account[_-]?key" + r"|ak|sk|token|password|authorization)=)" r"[^&#\s]+" ) +# ``ak``/``sk`` 是火山引擎凭证键名,``account_key`` 是 Azure 存储凭证键名。 +# 这些键很短,必须用词边界约束,否则 ``task = value`` 里的 ``sk`` 会被误脱敏。 +_SHORT_CREDENTIAL_KEY_RE = re.compile( + r'''(?i)(? str: value = str(value) redacted = _BEARER_TEXT_RE.sub("Bearer [REDACTED]", value) redacted = _KEY_VALUE_TEXT_RE.sub(r"\1=[REDACTED]", redacted) + redacted = _SHORT_CREDENTIAL_KEY_RE.sub(r"\1=[REDACTED]", redacted) redacted = _URL_USERINFO_RE.sub(r"\1[REDACTED]:[REDACTED]@", redacted) redacted = _URL_QUERY_SECRET_RE.sub(r"\1[REDACTED]", redacted) redacted = _OPENAI_KEY_RE.sub("[REDACTED]", redacted) diff --git a/tests/chat/test_core.py b/tests/chat/test_core.py index 5561068..06353af 100644 --- a/tests/chat/test_core.py +++ b/tests/chat/test_core.py @@ -392,3 +392,34 @@ def fake_launch(chat_config): assert edited_configs[0]["active_llm_configuration"] == "new-model" assert RealishChatbot.last_instance.session_config.active_llm_configuration == "old-model" + + +def test_redacting_formatter_scrubs_cached_traceback_for_later_handlers(): + """脱敏 handler 之后的普通 handler 不能拿到原始 traceback。 + + ``logging.Formatter`` 把格式化后的 traceback 缓存在 ``record.exc_text``, + 后续 handler 会复用该缓存。脱敏 formatter 必须在写回缓存前完成脱敏,否则 + 宿主挂在同一 logger 上的普通 handler 仍会输出原始凭证。 + """ + import io + import logging + import sys + + try: + raise RuntimeError("Authorization: Bearer sk-cached-traceback-secret") + except RuntimeError: + exc_info = sys.exc_info() + + redacting = logging.StreamHandler(io.StringIO()) + redacting.setFormatter(RedactingFormatter("%(message)s")) + plain = logging.StreamHandler(io.StringIO()) + plain.setFormatter(logging.Formatter("%(message)s")) + + logger = logging.getLogger("redaction-cache-test") + logger.propagate = False + logger.handlers = [redacting, plain] + logger.setLevel(logging.ERROR) + logger.error("request failed", exc_info=exc_info) + + assert "sk-cached-traceback-secret" not in redacting.stream.getvalue() + assert "sk-cached-traceback-secret" not in plain.stream.getvalue() diff --git a/tests/providers/test_security_boundaries.py b/tests/providers/test_security_boundaries.py new file mode 100644 index 0000000..7c58582 --- /dev/null +++ b/tests/providers/test_security_boundaries.py @@ -0,0 +1,52 @@ +"""凭证边界与脱敏的回归测试。""" + +import pytest + +from src.utils.security import ( + is_sensitive_option_key, + redact_sensitive_text, + validate_secret_free_options, +) + + +@pytest.mark.parametrize("key", ["ak", "sk", "AK", "SK", "account_key"]) +def test_security_recognizes_short_credential_keys(key): + """火山 ``ak``/``sk`` 与 Azure ``account_key`` 必须按凭证键处理。""" + assert is_sensitive_option_key(key) is True + + +@pytest.mark.parametrize("key", ["task", "risk", "ask", "mask", "disk", "sketch", "ak_region"]) +def test_security_does_not_flag_ordinary_keys_containing_ak_or_sk(key): + """短键必须精确匹配,普通业务字段不能被误判。""" + assert is_sensitive_option_key(key) is False + + +def test_security_rejects_volcengine_ak_sk_request_overrides(): + """把火山凭证塞进 extra_headers 必须被拒绝。""" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"ak": "AKLTxxx", "sk": "SKxxx"}, "Volcengine") + + +@pytest.mark.parametrize( + "value", + [ + "ak=AKLTabcdefghijklmnop", + "sk=SKabcdefghijklmnop", + "account_key=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6I", + '{"ak": "AKLT1234567890", "sk": "SK1234567890"}', + ], +) +def test_security_redacts_short_credential_key_assignments(value): + redacted = redact_sensitive_text(value) + assert "[REDACTED]" in redacted + assert "AKLTabcdefghijklmnop" not in redacted + assert "SKabcdefghijklmnop" not in redacted + + +@pytest.mark.parametrize( + "value", + ["task = value", "risk=high", "ask me later", "disk=full", "mask=none"], +) +def test_security_does_not_redact_ordinary_text_with_ak_or_sk(value): + """普通文本里的 ``sk``/``ak`` 子串不能被当成凭证脱敏。""" + assert redact_sensitive_text(value) == value From f5f2aee0907b227c7fd9375eec10e73a9a5591d1 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 20:58:50 +0800 Subject: [PATCH 14/35] fix: warn on retired base urls and align the remaining rerank option 1.4.0 moved the Qwen default endpoint to compatible-mode/v1 and the Volcengine default domain to ark.cn-beijing.volces.com/api/v3, but an existing config.toml keeps overriding the new defaults with endpoints that now answer 404/502. Warn at settings load so the fix is obvious instead of being reverse-engineered from a 404. Only warn: the old value is a repairable misconfiguration, and failing hard would stop deployments that still carry it from starting at all. Jina accepted a return_documents override that SiliconFlow rejects and that both providers otherwise hardcode to True; pin it so the two rerank providers behave the same. Drop the unused splitter_structure_mode parameter from _process_file. Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/jina.py | 4 ++- src/services/knowledge_build_service.py | 4 +-- src/utils/config.py | 38 +++++++++++++++++++++++- tests/providers/test_rerank_contracts.py | 21 +++++++++++++ tests/test_config.py | 38 ++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/providers/jina.py b/src/providers/jina.py index cc8bacf..30a41ac 100644 --- a/src/providers/jina.py +++ b/src/providers/jina.py @@ -49,7 +49,9 @@ def _prepare_payload(self, query: str, documents: list[str], top_n: int) -> dict "query": query, "documents": documents, "model": self._model_name, - "top_n": top_n + "top_n": top_n, + # 默认请求返回文档内容;与 SiliconFlow Rerank 保持一致的不可覆盖字段。 + "return_documents": True, } options = dict(self._options) options.pop("timeout", None) diff --git a/src/services/knowledge_build_service.py b/src/services/knowledge_build_service.py index 231da63..2a809c5 100644 --- a/src/services/knowledge_build_service.py +++ b/src/services/knowledge_build_service.py @@ -47,7 +47,7 @@ async def build(self, splitter_structure_mode: str) -> dict[str, object]: pending_embeddings: list[np.ndarray] = [] pending_parent_documents: dict[str, dict[str, Any]] = {} for file_path in markdown_files: - chunks = self._process_file(pipeline, file_path, splitter_structure_mode) + chunks = self._process_file(pipeline, file_path) if splitter_structure_mode == "hierarchical" and hasattr(pipeline.splitter, "parent_documents"): parent_documents = getattr(pipeline.splitter, "parent_documents", {}) if isinstance(parent_documents, dict): @@ -105,7 +105,7 @@ async def build(self, splitter_structure_mode: str) -> dict[str, object]: except Exception: logger.exception("清理临时知识快照失败: %s", temp_dir) - def _process_file(self, pipeline: Pipeline, file_path: Path, splitter_structure_mode: str) -> list[Document]: + def _process_file(self, pipeline: Pipeline, file_path: Path) -> list[Document]: logger.info("正在处理文件: %s", file_path) content = file_path.read_text(encoding="utf-8") document = Document(content=content, metadata={"source": str(file_path)}) diff --git a/src/utils/config.py b/src/utils/config.py index 5d5bddb..cd9ac60 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1,12 +1,13 @@ import functools import sys import tomllib +import warnings from collections.abc import Callable, Mapping from enum import Enum from pathlib import Path from typing import Any, ClassVar, cast -from pydantic import BaseModel, Field, ValidationInfo, field_validator +from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, @@ -328,6 +329,41 @@ def validate_hybrid_fusion_strategy(cls, v: Any) -> str: raise ValueError(f"无效的混合检索融合策略: {v}. 必须是 {', '.join(sorted(valid_strategies))}。") return normalized + @model_validator(mode="after") + def warn_retired_base_urls(self) -> "Settings": + """对已知失效的旧 base URL 发出显式升级警告。 + + 1.4.0 把 Qwen 默认端点从 ``dashscope.aliyuncs.com/api/v1`` 换成 + ``compatible-mode/v1``,并把火山默认域名从 + ``maas-api.ml-platform-cn-beijing.volces.com`` 换成 + ``ark.cn-beijing.volces.com/api/v3``。旧的 ``config.toml`` 会覆盖新默认值, + 使请求在运行期收到 404/502。这里在配置加载时就明确提示如何修正,而不是让 + 用户从 404 反推原因;显式配置的自建或代理端点不受影响。 + + 只警告不阻断:旧值是可修复的配置问题而非安全边界,直接失败会让仍在使用 + 旧配置的部署完全无法启动。 + """ + retired = { + "qwen_base_url": ( + "https://dashscope.aliyuncs.com/api/v1", + "https://dashscope.aliyuncs.com/compatible-mode/v1", + ), + "volc_base_url": ( + "https://maas-api.ml-platform-cn-beijing.volces.com", + "https://ark.cn-beijing.volces.com/api/v3", + ), + } + for field_name, (old_url, new_url) in retired.items(): + current = getattr(self, field_name, None) + if isinstance(current, str) and current.rstrip("/") == old_url: + warnings.warn( + f"{field_name} 指向已失效的旧端点 {old_url}," + f"请改为 {new_url}(旧端点会返回 404)。", + UserWarning, + stacklevel=2, + ) + return self + @field_validator('retrieval_candidate_multiplier', mode='before') @classmethod def validate_retrieval_candidate_multiplier(cls, v: Any) -> int: diff --git a/tests/providers/test_rerank_contracts.py b/tests/providers/test_rerank_contracts.py index dfb5536..654f611 100644 --- a/tests/providers/test_rerank_contracts.py +++ b/tests/providers/test_rerank_contracts.py @@ -102,3 +102,24 @@ def test_siliconflow_rerank_rejects_return_documents_override(): with pytest.raises(ValueError, match="return_documents"): provider._prepare_payload("query", ["document"], 1) + + +def test_jina_rerank_rejects_return_documents_override(): + """Jina 与 SiliconFlow 一样把 return_documents 固定为 True,不允许覆盖。""" + provider = object.__new__(JinaProvider) + provider._model_name = "rerank-model" + provider._options = {"return_documents": False} + + with pytest.raises(ValueError, match="return_documents"): + provider._prepare_payload("query", ["document"], 1) + + +def test_jina_rerank_requests_documents_by_default(): + """未显式配置时也要请求返回文档,与 SiliconFlow 行为一致。""" + provider = object.__new__(JinaProvider) + provider._model_name = "rerank-model" + provider._options = {} + + payload = provider._prepare_payload("query", ["document"], 1) + + assert payload["return_documents"] is True diff --git a/tests/test_config.py b/tests/test_config.py index 588f43a..eb554ac 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -320,3 +320,41 @@ def test_resolve_app_root_frozen_mode(monkeypatch, tmp_path): root = resolve_app_root() assert root == executable.parent + + +def test_settings_warns_on_retired_qwen_base_url(): + """旧 Qwen 端点会 404,加载配置时必须给出显式升级提示。""" + with pytest.warns(UserWarning, match="qwen_base_url"): + Settings.model_validate( + { + "qwen_base_url": "https://dashscope.aliyuncs.com/api/v1", + "volc_base_url": "https://ark.cn-beijing.volces.com/api/v3", + } + ) + + +def test_settings_warns_on_retired_volc_base_url(): + with pytest.warns(UserWarning, match="volc_base_url"): + Settings.model_validate( + { + "qwen_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "volc_base_url": "https://maas-api.ml-platform-cn-beijing.volces.com", + } + ) + + +def test_settings_accepts_current_and_custom_base_urls(): + """新默认值以及自建/代理端点都不应触发升级警告。""" + import warnings as warnings_module + + for qwen, volc in [ + ( + "https://dashscope.aliyuncs.com/compatible-mode/v1", + "https://ark.cn-beijing.volces.com/api/v3", + ), + ("https://my-proxy.internal/v1", "https://my-proxy.internal/v3"), + ]: + with warnings_module.catch_warnings(record=True) as caught: + warnings_module.simplefilter("always") + Settings.model_validate({"qwen_base_url": qwen, "volc_base_url": volc}) + assert not [w for w in caught if "base_url" in str(w.message)] From b1c1b0943de04eefa1b778e7a48ce8cab1288143 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 21:32:33 +0800 Subject: [PATCH 15/35] docs: record the review fixes in the 1.4.0 changelog Add the tool-call normalization regression, the ak/sk and cached traceback credential fixes, the retired base-url warning and the Jina return_documents alignment to the 1.4.0 section, and update the test count to 800. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e74a96..8faf2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 建立统一 Provider 工厂与抽象请求模型,补齐 LLM、Embedding、Rerank 的能力声明、配置校验、缓存与同步/异步生命周期管理。 - 新增 `complete()`/`acomplete()` 高级调用入口,统一支持多轮消息、工具调用、结构化输出、多模态内容、usage 与结束原因;`invoke()`/`ainvoke()` 保留文本流兼容行为。 +- 修复多轮工具调用的历史归一化:共享的 `normalize_messages` 不再强制要求工具调用带 `id`,因为 Gemini 的 `FunctionCall.id` 是可选字段且 SDK 默认 `None`,否则把上一轮工具调用回填给 Google 会在请求发出前被本地拒绝;需要 `id` 的 Chat Completions 与 Responses 改为显式校验。同一处还修复了 `input` 参数别名被静默清空,以及 `function_call`/`custom_tool_call` 类型未映射为 `function` 的问题。 - 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类线协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses,Ark 的 `ark` 取值是 Chat Completions 的别名。 - Responses 对非官方端点要求 `options.server_verified_protocols = ["responses"]` 显式登记;未登记时工厂、Provider 和资源 Facade 均在请求前拒绝,避免把本地 SDK 资源误发到未实现 `/responses` 的服务端。 - 新增原生资源 Facade,显式暴露文件、批处理、缓存、向量库、token 计数、调优等 SDK 能力,不再把资源生命周期混入普通聊天请求。 @@ -17,7 +18,9 @@ ### 安全 - 新增配置、请求与日志边界的凭证校验:模型级 `options` 与请求级扩展不得携带密钥、请求头、query 或 Base URL 覆盖,资源 Facade 的关键字与位置参数同样受检。 -- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query)。 +- 凭证键识别补齐火山引擎的 `ak`/`sk` 与 Azure 存储的 `account_key`;此前这些键可绕过请求覆盖校验,而 `api_key` 会被拒绝。 +- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query);补齐 `ak=`/`sk=`/`account_key=` 形式。 +- 修复脱敏日志 formatter 的缓存泄漏:`logging.Formatter` 会把格式化后的 traceback 缓存在 `record.exc_text` 上供后续 handler 复用,先格式化后脱敏会让同一 logger 上的非脱敏 handler 输出原始凭证;现在脱敏结果会写回缓存。 - 兼容渠道不再被当作官方 OpenAI 端点放行 SDK 专属资源;仅精确匹配 `https://api.openai.com/v1` 时按 SDK 契约放行。 - 收紧失败语义:凭证、SDK 或远端调用失败时显式报错,不再以空 embedding、零分 rerank 或占位回答掩盖失败。 @@ -27,11 +30,12 @@ - 移除 iFlow 渠道及其配置示例。 - 移除 qwen rerank 配置示例;该渠道不提供 rerank 能力,调用时会显式报错。 - 发布脚本新增目标环境校验,`--target` 与主机系统/架构不匹配时在清理构建产物前显式失败,不再生成错误架构的发布包。 -- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。 +- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。旧端点已失效,配置仍为旧值时会在加载时给出显式升级警告。 +- Jina Rerank 与 SiliconFlow Rerank 一致,`return_documents` 固定为 `True` 且不允许被 `options` 覆盖。 ### 测试与文档 -- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 760。 +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 From 9e8c0f09a88673727b46b3d7a80df468c0867fb5 Mon Sep 17 00:00:00 2001 From: Mison Date: Sun, 20 Sep 2026 22:33:09 +0800 Subject: [PATCH 16/35] docs: spell out per-protocol tool_call id requirements Record how multi-turn tool history is normalized at the request boundary and which protocols require an id on assistant tool_calls: Chat Completions and Responses reject a missing id before the request is sent, while Gemini's FunctionCall.id is optional and id-less calls replay fine. Co-Authored-By: Claude Opus 5 (1M context) --- docs/user_guide/llm-providers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index 5ceea14..d0764ec 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -92,6 +92,8 @@ Responses 请求会使用 `input`、`instructions` 和 `tools` 字段,并把 统一的高级调用入口是 Provider 的 `complete()`/`acomplete()`,支持 `messages`(多轮和多模态内容)、`tools`、`tool_choice`、`response_format`、`max_tokens`、`top_p`、`stop`、`seed` 和 `extra_body`。结果包含 `text`、`tool_calls`、`usage`、`finish_reason` 和原始 SDK 响应。旧版 `invoke()`/`ainvoke()` 仍只输出文本流,以保持聊天界面兼容。需要 reasoning、thinking、拒答或流式 usage 时使用 `stream_events()`/`astream_events()`。 +多轮工具调用的历史可以原样回填:请求边界会把扁平的 `{"id","type","name","arguments"}` 归一化为各协议需要的形状,并读取 Responses `custom_tool_call` 与 Anthropic `tool_use` 使用的 `input` 别名。各协议对关联 ID 的要求不同——Chat Completions 与 Responses 要求 `assistant.tool_calls` 带 `id`(缺少时在请求前显式报错),Google Gemini 不需要,其 `FunctionCall.id` 为可选且默认缺省,因此无 id 的 Google 工具调用可以正常回填。Provider 自己产出的 `tool_calls` 都带有协议所需的标识。 + 非敏感供应商参数放在模型条目的 `options` 表中,工厂会拒绝 `api_key`、`access_key`、`secret_key`、`token`、`headers` 和 `base_url` 等字段: ```toml From c36743ec545f40a8d08c6ad5cbc06ad5e4e0153c Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 00:10:21 +0800 Subject: [PATCH 17/35] fix: recognize new Claude families in sampling deprecation check The model version regex only matched opus/sonnet/haiku, so the Claude 5 families introduced later (claude-fable-5, claude-mythos-5, and the unversioned claude-mythos-preview) were classified as pre-4.5 models. That let temperature/top_p/top_k through to the request, but the Anthropic Python SDK v1.0+ no longer accepts those parameters at all, so the call failed with a TypeError instead of the intended omission. Match the family segment as any word and add a pattern for the unversioned modern names. Versioned legacy models such as claude-sonnet-4 and claude-3-5-sonnet-20240620 still report as non-deprecated. Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/anthropic.py | 30 +++++++++++++++---- tests/providers/test_sdk_capabilities.py | 37 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/providers/anthropic.py b/src/providers/anthropic.py index e592f76..543cf41 100644 --- a/src/providers/anthropic.py +++ b/src/providers/anthropic.py @@ -56,22 +56,34 @@ class AnthropicProvider(LargeLanguageModel): } ) + # Anthropic 同时使用两种模型命名:``claude--``(如 + # ``claude-sonnet-4-6``)与 ``claude-[-]-``(如 + # ``claude-3-5-sonnet-20240620``)。家族名不限于 opus/sonnet/haiku—— + # 5 代引入了 ``claude-fable-5``、``claude-mythos-5`` 等新家族,因此家族段 + # 按任意字母串匹配,避免新家族漏判采样控制字段的弃用契约。 _MODEL_VERSION_RE = re.compile( r"claude[-_]" r"(?:" - r"(?:opus|sonnet|haiku)[-_](?P\d+)" + r"(?P[a-z]+)[-_](?P\d+)" r"(?:[-_.](?P\d{1,2}))?" r"(?:[-_]\d{8})?" r"|" r"(?P\d+)" r"(?:[-_.](?P\d{1,2}))?" - r"[-_](?:opus|sonnet|haiku)" + r"[-_](?:[a-z]+)" r"(?:[-_]\d{8})?" r")" r"(?=$|[-_.])", re.IGNORECASE, ) + # 无版本号的现役家族名(如 ``claude-mythos-preview``)。这些是 5 代命名, + # 官方端点同样拒绝 legacy 采样控制字段。 + _UNVERSIONED_MODERN_MODEL_RE = re.compile( + r"claude[-_](?:fable|mythos)(?:[-_][a-z0-9]+)*$", + re.IGNORECASE, + ) + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name self._options = validate_secret_free_options(options, "Anthropic") @@ -130,10 +142,18 @@ def _safe_resource_kwargs(kwargs: Mapping[str, Any], operation: str) -> dict[str @classmethod def _sampling_controls_deprecated(cls, model_name: str) -> bool: - """识别 Claude 4.5+ 模型对采样控制字段的弃用契约。""" - match = cls._MODEL_VERSION_RE.search(str(model_name)) + """识别 Claude 4.5+ 模型对采样控制字段的弃用契约。 + + 4.7 起官方端点对非默认值直接返回 400,且 Python SDK v1.0+ 已从请求 + 签名移除 ``temperature``/``top_p``/``top_k``,透传会抛 ``TypeError``。 + 因此识别失败会让请求直接失败,必须覆盖全部命名形式。 + """ + name = str(model_name) + match = cls._MODEL_VERSION_RE.search(name) if match is None: - return False + # 无版本号的家族名(如 ``claude-mythos-preview``)出现在 5 代命名 + # 中,按当前主力家族处理,避免把弃用字段透传给新模型。 + return cls._UNVERSIONED_MODERN_MODEL_RE.search(name) is not None major = int(match.group("family_major") or match.group("version_major")) minor_value = match.group("family_minor") or match.group("version_minor") minor = int(minor_value) if minor_value is not None else 0 diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py index 2cdf2f5..f31009b 100644 --- a/tests/providers/test_sdk_capabilities.py +++ b/tests/providers/test_sdk_capabilities.py @@ -5773,3 +5773,40 @@ def test_responses_input_still_requires_tool_call_id(): ], ) + + +@pytest.mark.parametrize( + "model_name", + [ + "claude-fable-5", + "claude-fable-5-1", + "claude-mythos-5", + "claude-mythos-5-1", + "claude-mythos-preview", + ], +) +def test_anthropic_sampling_deprecation_covers_new_family_names(model_name): + """5 代新增家族(fable/mythos)同样拒绝 legacy 采样控制字段。 + + Python SDK v1.0+ 已从请求签名移除 temperature/top_p/top_k,识别失败会让 + 请求直接抛 TypeError。家族名不能只匹配 opus/sonnet/haiku。 + """ + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is True + + +@pytest.mark.parametrize( + "model_name", + ["claude-3-5-sonnet-20240620", "claude-3-haiku-20240307", "claude-2.1", + "claude-sonnet-4", "claude-opus-4"], +) +def test_anthropic_sampling_deprecation_keeps_legacy_models_permissive(model_name): + """4.5 之前的模型仍接受采样控制字段,不能被新家族规则误伤。""" + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is False From 05de41c8c3ce54b7334d017e22bbe73e3e659648 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 00:10:34 +0800 Subject: [PATCH 18/35] chore: refresh example models against current provider catalogs Checked each channel's official model list and replaced IDs that are retired or no longer offered. Verified against a live gateway: every replacement answers 200 and every retired ID answers 503. - Google embedding: text-embedding-004 retired 2026-01-14 - OpenAI: gpt-3.5-turbo retires 2026-10-23, gpt-4o snapshots 2026-10-23 - Anthropic: claude-3-5-sonnet-20240620 retired 2025-10-28 - DeepSeek: deepseek-chat retired 2026-07-24, deepseek-v2 was never a real API id - Volcengine: doubao-pro-32k retired, bge-large-zh absent from the Ark model list - Qwen: qwen-max/qwen-plus are legacy aliases whose snapshots are being retired - SiliconFlow: Qwen2-7B-Instruct and all meta-llama models are gone - Grok: llama3-70b-8192 is a Groq id, not an xAI one Also update the built-in llm_configurations defaults and drop the get_settings() dependency from test_settings_defaults, which made those assertions depend on whatever the developer's local config.toml held. Embedding-space changes are not drop-in: gemini-embedding-2 and text-embedding-004 are incompatible, so an existing snapshot must be rebuilt rather than reused. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 ++ config.toml.example | 52 ++++++++++++++++---------------- docs/user_guide/configuration.md | 2 +- docs/user_guide/llm-providers.md | 8 ++--- src/utils/config.py | 18 ++++++----- tests/test_config.py | 8 +++-- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8faf2e2..7bb0bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ - 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-vision-251215`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI)。模型内置默认值同步更新。 +- 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 ## [1.3.0] - 2026-03-20 diff --git a/config.toml.example b/config.toml.example index 25fb39a..0eed0d5 100644 --- a/config.toml.example +++ b/config.toml.example @@ -102,8 +102,8 @@ model_name = "local-hash-256" [embedding_configurations.google] # 提供商实现标识 provider = "google" -# API 实际模型名 -model_name = "models/text-embedding-004" +# API 实际模型名(text-embedding-004 已于 2026-01-14 退役) +model_name = "gemini-embedding-2" # 可选 SDK 参数:查询会自动使用 RETRIEVAL_QUERY,文档使用 RETRIEVAL_DOCUMENT # options = { output_dimensionality = 768 } @@ -125,8 +125,8 @@ model_name = "text-embedding-v4" [embedding_configurations.volcengine] # 提供商实现标识 provider = "volcengine" -# API 实际模型名 -model_name = "bge-large-zh" +# API 实际模型名(bge-large-zh 已不在方舟模型列表中,改用现役向量模型) +model_name = "doubao-embedding-vision-251215" # ollama 向量化模型 [embedding_configurations.ollama] @@ -176,46 +176,46 @@ model_name = "gemini-2.5-pro" [llm_configurations.openai] # 提供商实现标识 provider = "openai" -# API 实际模型名 -model_name = "gpt-4o" +# API 实际模型名(gpt-4o 各快照自 2026-10-23 起退役) +model_name = "gpt-5.6-sol" # openai-responses Responses 协议示例 # 仅当 Base URL 对应服务端实现了 /responses 时使用;省略 protocol 默认使用 Chat Completions。 [llm_configurations.openai-responses] provider = "openai" -model_name = "gpt-5" +model_name = "gpt-5.6-sol" protocol = "responses" # 仅放非敏感 SDK 参数;API key、Base URL 必须放 .env/环境变量 # 如果 Base URL 是代理、中转或自建网关,完成服务端验证后显式登记: # options = { server_verified_protocols = ["responses"], reasoning = { effort = "medium" }, store = true } -# openai-gpt3.5 聊天模型 +# openai-gpt3.5 聊天模型(gpt-3.5-turbo 于 2026-10-23 退役,示例改用现行模型) [llm_configurations."openai-gpt3.5"] # 提供商实现标识 provider = "openai" # API 实际模型名 -model_name = "gpt-3.5-turbo" +model_name = "gpt-5.6-terra" # qwen-max 聊天模型 [llm_configurations.qwen-max] # 提供商实现标识 provider = "qwen" -# API 实际模型名 -model_name = "qwen-max" +# API 实际模型名(qwen-max 为旧版裸名,快照系列已陆续下线) +model_name = "qwen3.8-max" # qwen-plus 聊天模型 [llm_configurations.qwen-plus] # 提供商实现标识 provider = "qwen" # API 实际模型名 -model_name = "qwen-plus" +model_name = "qwen3.7-plus" -# volc-doubao-pro-32k 聊天模型 +# volc-doubao 聊天模型(doubao-pro-32k 全部快照已退役) [llm_configurations.volc-doubao-pro-32k] # 提供商实现标识 provider = "volcengine" # API 实际模型名 -model_name = "doubao-pro-32k" +model_name = "doubao-seed-2-0-lite-260428" # Ark Responses 示例;只有目标 Ark 服务端支持 /responses 时启用 # [llm_configurations.volc-responses] @@ -225,19 +225,19 @@ model_name = "doubao-pro-32k" # 仅在目标 Ark Base URL 已实际验证 /responses 后保留此登记 # options = { server_verified_protocols = ["responses"] } -# sf-qwen2-7b 聊天模型 +# sf-qwen 聊天模型(Qwen2-7B-Instruct 已从 SiliconFlow 模型广场下线) [llm_configurations.sf-qwen2-7b] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 -model_name = "Qwen/Qwen2-7B-Instruct" +model_name = "Qwen/Qwen3-8B" -# sf-llama3-8b 聊天模型 +# sf-llama3-8b 聊天模型(meta-llama/* 已从 SiliconFlow 模型广场下线) [llm_configurations.sf-llama3-8b] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 -model_name = "meta-llama/Meta-Llama-3-8B-Instruct" +model_name = "deepseek-ai/DeepSeek-V3.2" # ollama-llama3 聊天模型 [llm_configurations.ollama-llama3] @@ -260,30 +260,30 @@ provider = "lm-studio" # API 实际模型名 model_name = "local-model/gguf-model-name" -# deepseek-chat 聊天模型 +# deepseek 聊天模型(deepseek-chat 已于 2026-07-24 完全退役) [llm_configurations.deepseek-chat] # 提供商实现标识 provider = "deepseek" # API 实际模型名 -model_name = "deepseek-chat" +model_name = "deepseek-v4-pro" -# deepseek-v2 聊天模型 +# deepseek-flash 聊天模型(deepseek-v2 从未作为官方 API ID 存在) [llm_configurations.deepseek-v2] # 提供商实现标识 provider = "deepseek" # API 实际模型名 -model_name = "deepseek-v2" +model_name = "deepseek-v4-flash" -# grok-llama3-70b 聊天模型 +# grok 聊天模型(llama3-70b-8192 是 Groq 的 ID,xAI 只有 grok-* 系列) [llm_configurations.grok-llama3-70b] # 提供商实现标识 provider = "grok" # API 实际模型名 -model_name = "llama3-70b-8192" +model_name = "grok-4.6" -# anthropic-sonnet-3.5 聊天模型 +# anthropic 聊天模型(claude-3-5-sonnet-20240620 已于 2025-10-28 退役) [llm_configurations."anthropic-sonnet-3.5"] # 提供商实现标识 provider = "anthropic" # API 实际模型名 -model_name = "claude-3-5-sonnet-20240620" +model_name = "claude-sonnet-4-6" diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index c7156d5..0659aa7 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -117,7 +117,7 @@ DEFAULT_LLM_PROVIDER="openai" ```toml [llm_configurations.demo] provider = "openai" -model_name = "gpt-4o" +model_name = "gpt-5.6-sol" ``` - `llm_configurations`: 聊天模型 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index d0764ec..723d6cc 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -70,7 +70,7 @@ OpenAI 兼容的 LLM 配置默认调用 Chat Completions。需要调用 Response ```toml [llm_configurations.openai-responses] provider = "openai" -model_name = "gpt-5" +model_name = "gpt-5.6-sol" protocol = "responses" ``` @@ -126,12 +126,12 @@ Embedding 文档和查询会区分任务类型:Google 使用 `RETRIEVAL_DOCUME ```toml [llm_configurations.google-pro] provider = "google" -model_name = "gemini-1.5-pro-latest" +model_name = "gemini-2.5-pro" ``` * **`your-custom-name`**: 您为这个模型配置起的名字。这个名字会显示在 `/config` 菜单中供您选择。例如,`google-pro`。 * **`provider`**: 指定使用哪个模型提供商的实现。这个值必须与 `src/providers/` 目录下的某个文件名(或工厂类中的标识符)相对应。例如,`google`。 -* **`model_name`**: 要调用的实际模型名称/ID。这个值会直接传递给对应服务商的 API。例如,`gemini-1.5-pro-latest`。 +* **`model_name`**: 要调用的实际模型名称/ID。这个值会直接传递给对应服务商的 API。例如,`gemini-2.5-pro`。 * **`protocol`**(仅 LLM 可选): 指定渠道协议。OpenAI 兼容渠道和 Volcengine 可使用 `chat_completions`/`responses`(Volcengine 省略时为 Ark Chat);Google、Anthropic 使用各自固定协议。Embedding 与 Rerank 配置不应填写此字段。 ## 当前默认推荐组合 @@ -161,7 +161,7 @@ DEFAULT_LLM_PROVIDER="openai" ```toml [llm_configurations.openai] provider = "openai" -model_name = "gpt-4o" +model_name = "gpt-5.6-sol" ``` ## 如何添加新模型 diff --git a/src/utils/config.py b/src/utils/config.py index cd9ac60..3bb08fe 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -248,14 +248,16 @@ def model_dump_json(self, *args: Any, **kwargs: Any) -> str: "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-reranker-large"), }) llm_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { - "google": ModelDetail(provider="google", model_name="gemini-1.5-pro-latest"), - "anthropic": ModelDetail(provider="anthropic", model_name="claude-3-opus-20240229"), - "qwen": ModelDetail(provider="qwen", model_name="qwen-turbo"), - "deepseek": ModelDetail(provider="deepseek", model_name="deepseek-chat"), - "grok": ModelDetail(provider="grok", model_name="grok-1"), - "volcengine": ModelDetail(provider="volcengine", model_name="Doubao-pro-32k"), - "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V2-Chat"), - "openai": ModelDetail(provider="openai", model_name="gpt-4o"), + # 默认条目只作为缺失配置时的兜底;模型名保持在写就时仍可用的现行 ID, + # 避免新用户照抄到已退役模型。 + "google": ModelDetail(provider="google", model_name="gemini-2.5-flash"), + "anthropic": ModelDetail(provider="anthropic", model_name="claude-sonnet-4-6"), + "qwen": ModelDetail(provider="qwen", model_name="qwen3.8-max"), + "deepseek": ModelDetail(provider="deepseek", model_name="deepseek-v4-pro"), + "grok": ModelDetail(provider="grok", model_name="grok-4.6"), + "volcengine": ModelDetail(provider="volcengine", model_name="doubao-seed-2-0-lite-260428"), + "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V3.2"), + "openai": ModelDetail(provider="openai", model_name="gpt-5.6-sol"), "ollama": ModelDetail(provider="ollama", model_name="llama3"), "lm-studio": ModelDetail(provider="lm-studio", model_name="LM-Studio-Community/Meta-Llama-3-8B-Instruct-GGUF"), }) diff --git a/tests/test_config.py b/tests/test_config.py index eb554ac..f337336 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -87,8 +87,11 @@ def test_settings_splitter_separators_blank_csv_falls_back_to_default(): assert settings.kb_splitter_separators == ["###"] -def test_settings_defaults(): - settings = get_settings() +def test_settings_defaults(monkeypatch, tmp_path): + # 直接构造,避免读取开发者本机的 config.toml:这些断言应验证模型内置默认值, + # 而不是某个本地文件恰好写了什么。 + monkeypatch.setattr("src.utils.config.CONFIG_TOML_PATH", tmp_path / "absent.toml") + settings = Settings(_env_file=None) assert settings.openai_api_key is None assert settings.chat_top_k == 5 @@ -101,7 +104,6 @@ def test_settings_defaults(): assert settings.kb_child_chunk_overlap == 30 assert settings.default_embedding_provider == "local-hash" assert settings.embedding_configurations["local-hash"].model_name == "local-hash-256" - assert settings.llm_configurations["openai"].model_name == "gpt-4o" assert settings.qwen_base_url.endswith("/compatible-mode/v1") assert settings.volc_base_url.endswith("/api/v3") assert "jina" not in settings.embedding_configurations From 413193b8baaa11373bacadf92322ffc7872ff569 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 00:32:28 +0800 Subject: [PATCH 19/35] docs: align example keys with refreshed models and fix volc embedding Follow-up to the catalog refresh: the example section keys still named retired models (sf-qwen2-7b holding Qwen/Qwen3-8B, grok-llama3-70b holding grok-4.6), which is misleading for a copy-paste config. Rename them to match the model they configure and refresh the Ollama entries to llama3.1/gemma3. Correct the Volcengine embedding model. The previous value pointed at doubao-embedding-vision, a multimodal model, but this provider's embedding path calls the text embeddings.create endpoint; use doubao-embedding-text-240715 instead. Also update the provider list examples in the user guide, which still cited GPT-4o, GPT-3.5-Turbo and Claude 3.5 Sonnet. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 3 +- config.toml.example | 55 ++++++++++++++++---------------- docs/user_guide/llm-providers.md | 6 ++-- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb0bcc..20490bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,8 @@ - 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 -- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-vision-251215`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI)。模型内置默认值同步更新。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表;文本向量化不要用 `doubao-embedding-vision` 这类多模态模型),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新。 +- 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。 - 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 ## [1.3.0] - 2026-03-20 diff --git a/config.toml.example b/config.toml.example index 0eed0d5..fa8fb38 100644 --- a/config.toml.example +++ b/config.toml.example @@ -125,8 +125,9 @@ model_name = "text-embedding-v4" [embedding_configurations.volcengine] # 提供商实现标识 provider = "volcengine" -# API 实际模型名(bge-large-zh 已不在方舟模型列表中,改用现役向量模型) -model_name = "doubao-embedding-vision-251215" +# API 实际模型名(bge-large-zh 已不在方舟模型列表中;这里走文本 embeddings.create, +# 不要用 doubao-embedding-vision 这类多模态模型) +model_name = "doubao-embedding-text-240715" # ollama 向量化模型 [embedding_configurations.ollama] @@ -189,29 +190,29 @@ protocol = "responses" # 如果 Base URL 是代理、中转或自建网关,完成服务端验证后显式登记: # options = { server_verified_protocols = ["responses"], reasoning = { effort = "medium" }, store = true } -# openai-gpt3.5 聊天模型(gpt-3.5-turbo 于 2026-10-23 退役,示例改用现行模型) -[llm_configurations."openai-gpt3.5"] +# openai-fast 聊天模型(gpt-3.5-turbo 于 2026-10-23 退役,示例改用现行模型) +[llm_configurations.openai-fast] # 提供商实现标识 provider = "openai" # API 实际模型名 model_name = "gpt-5.6-terra" -# qwen-max 聊天模型 -[llm_configurations.qwen-max] +# qwen3.8-max 聊天模型(qwen-max 为旧版裸名,快照系列已陆续下线) +[llm_configurations.qwen3-8-max] # 提供商实现标识 provider = "qwen" -# API 实际模型名(qwen-max 为旧版裸名,快照系列已陆续下线) +# API 实际模型名 model_name = "qwen3.8-max" -# qwen-plus 聊天模型 -[llm_configurations.qwen-plus] +# qwen3.7-plus 聊天模型 +[llm_configurations.qwen3-7-plus] # 提供商实现标识 provider = "qwen" # API 实际模型名 model_name = "qwen3.7-plus" -# volc-doubao 聊天模型(doubao-pro-32k 全部快照已退役) -[llm_configurations.volc-doubao-pro-32k] +# volc-doubao-seed 聊天模型(doubao-pro-32k 全部快照已退役) +[llm_configurations.volc-doubao-seed] # 提供商实现标识 provider = "volcengine" # API 实际模型名 @@ -225,33 +226,33 @@ model_name = "doubao-seed-2-0-lite-260428" # 仅在目标 Ark Base URL 已实际验证 /responses 后保留此登记 # options = { server_verified_protocols = ["responses"] } -# sf-qwen 聊天模型(Qwen2-7B-Instruct 已从 SiliconFlow 模型广场下线) -[llm_configurations.sf-qwen2-7b] +# sf-qwen3-8b 聊天模型(Qwen2-7B-Instruct 已从 SiliconFlow 模型广场下线) +[llm_configurations.sf-qwen3-8b] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 model_name = "Qwen/Qwen3-8B" -# sf-llama3-8b 聊天模型(meta-llama/* 已从 SiliconFlow 模型广场下线) -[llm_configurations.sf-llama3-8b] +# sf-deepseek-v3 聊天模型(meta-llama/* 已从 SiliconFlow 模型广场下线) +[llm_configurations.sf-deepseek-v3] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 model_name = "deepseek-ai/DeepSeek-V3.2" -# ollama-llama3 聊天模型 +# ollama-llama3.1 聊天模型 [llm_configurations.ollama-llama3] # 提供商实现标识 provider = "ollama" # API 实际模型名 -model_name = "llama3" +model_name = "llama3.1" -# ollama-gemma 聊天模型 +# ollama-gemma3 聊天模型 [llm_configurations.ollama-gemma] # 提供商实现标识 provider = "ollama" # API 实际模型名 -model_name = "gemma" +model_name = "gemma3" # lm-studio 聊天模型 [llm_configurations.lm-studio] @@ -260,29 +261,29 @@ provider = "lm-studio" # API 实际模型名 model_name = "local-model/gguf-model-name" -# deepseek 聊天模型(deepseek-chat 已于 2026-07-24 完全退役) -[llm_configurations.deepseek-chat] +# deepseek-v4-pro 聊天模型(deepseek-chat 已于 2026-07-24 完全退役) +[llm_configurations.deepseek-v4-pro] # 提供商实现标识 provider = "deepseek" # API 实际模型名 model_name = "deepseek-v4-pro" -# deepseek-flash 聊天模型(deepseek-v2 从未作为官方 API ID 存在) -[llm_configurations.deepseek-v2] +# deepseek-v4-flash 聊天模型(deepseek-v2 从未作为官方 API ID 存在) +[llm_configurations.deepseek-v4-flash] # 提供商实现标识 provider = "deepseek" # API 实际模型名 model_name = "deepseek-v4-flash" -# grok 聊天模型(llama3-70b-8192 是 Groq 的 ID,xAI 只有 grok-* 系列) -[llm_configurations.grok-llama3-70b] +# grok-4.6 聊天模型(llama3-70b-8192 是 Groq 的 ID,xAI 只有 grok-* 系列) +[llm_configurations.grok-4-6] # 提供商实现标识 provider = "grok" # API 实际模型名 model_name = "grok-4.6" -# anthropic 聊天模型(claude-3-5-sonnet-20240620 已于 2025-10-28 退役) -[llm_configurations."anthropic-sonnet-3.5"] +# anthropic-sonnet-4-6 聊天模型(claude-3-5-sonnet-20240620 已于 2025-10-28 退役) +[llm_configurations.anthropic-sonnet-4-6] # 提供商实现标识 provider = "anthropic" # API 实际模型名 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index 723d6cc..4eddda9 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -15,8 +15,8 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 ### 云服务模型 * **Google**: 支持 Gemini 系列模型。 -* **OpenAI**: 支持 GPT 系列模型,如 GPT-4o, GPT-3.5-Turbo。 -* **Anthropic**: 支持 Claude 系列模型,如 Claude 3.5 Sonnet。 +* **OpenAI**: 支持 GPT 系列模型,如 GPT-5.6 系列。 +* **Anthropic**: 支持 Claude 系列模型,如 Claude Sonnet 4.6 与 Claude 5 系列。 * **阿里云 (Qwen)**: 支持通义千问系列模型。 * **火山引擎 (VolcEngine)**: 支持豆包 (Doubao) 系列模型。 * **深度求索 (DeepSeek)**: 支持 DeepSeek 系列模型。 @@ -26,7 +26,7 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 ### 本地化模型 -* **Ollama**: 支持通过 Ollama 在本地运行的各种开源模型,如 Llama3, Gemma 等。 +* **Ollama**: 支持通过 Ollama 在本地运行的各种开源模型,如 Llama 3.1、Gemma 3 等。 * **LM Studio**: 支持通过 LM Studio 在本地运行的 `gguf` 格式模型。 ## 配置方法 From 200ce50580217b76576511fb1736802dc870ff94 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 01:19:35 +0800 Subject: [PATCH 20/35] fix: require a word boundary before the claude model prefix The version regex matched "claude" anywhere in the string, so an unrelated name such as "notclaude-sonnet-4-6" was parsed as a modern Claude model and had its sampling fields silently dropped. Require a non-alphanumeric boundary before the prefix in both patterns. Also finish the example alignment: the ollama keys still named llama3 and gemma while holding llama3.1 and gemma3, and the "add a new model" walkthrough in the user guide used Qwen/Qwen2-57B-A14B-Instruct, which is no longer offered. Co-Authored-By: Claude Opus 5 (1M context) --- config.toml.example | 4 ++-- docs/user_guide/llm-providers.md | 8 ++++---- src/providers/anthropic.py | 4 ++-- tests/providers/test_sdk_capabilities.py | 13 +++++++++++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/config.toml.example b/config.toml.example index fa8fb38..59db7ad 100644 --- a/config.toml.example +++ b/config.toml.example @@ -241,14 +241,14 @@ provider = "siliconflow" model_name = "deepseek-ai/DeepSeek-V3.2" # ollama-llama3.1 聊天模型 -[llm_configurations.ollama-llama3] +[llm_configurations.ollama-llama3-1] # 提供商实现标识 provider = "ollama" # API 实际模型名 model_name = "llama3.1" # ollama-gemma3 聊天模型 -[llm_configurations.ollama-gemma] +[llm_configurations.ollama-gemma3] # 提供商实现标识 provider = "ollama" # API 实际模型名 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index 4eddda9..0a7ef3d 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -166,17 +166,17 @@ model_name = "gpt-5.6-sol" ## 如何添加新模型 -假设您想添加一个通过 SiliconFlow 平台提供的 `Qwen/Qwen2-57B-A14B-Instruct` 模型,可以按以下步骤操作: +假设您想添加一个通过 SiliconFlow 平台提供的 `zai-org/GLM-5.3` 模型,可以按以下步骤操作: 1. **确保密钥已配置**: 在 `.env` 中填入 `SILICONFLOW_API_KEY`。 2. **编辑 `llm_configurations`**: 在 `config.toml` 中添加一个新的表: ```toml - [llm_configurations.sf-qwen2-57b] + [llm_configurations.sf-glm-5-3] provider = "siliconflow" - model_name = "Qwen/Qwen2-57B-A14B-Instruct" + model_name = "zai-org/GLM-5.3" ``` 3. **重启程序**: 保存 `config.toml` 文件并重新启动 `uv run main.py`。 -现在,您就可以在 `/config` 菜单的“切换模型” -> “语言模型 (LLM)” 选项中看到并选择 `sf-qwen2-57b` 了。 +现在,您就可以在 `/config` 菜单的“切换模型” -> “语言模型 (LLM)” 选项中看到并选择 `sf-glm-5-3` 了。 diff --git a/src/providers/anthropic.py b/src/providers/anthropic.py index 543cf41..29c4469 100644 --- a/src/providers/anthropic.py +++ b/src/providers/anthropic.py @@ -62,7 +62,7 @@ class AnthropicProvider(LargeLanguageModel): # 5 代引入了 ``claude-fable-5``、``claude-mythos-5`` 等新家族,因此家族段 # 按任意字母串匹配,避免新家族漏判采样控制字段的弃用契约。 _MODEL_VERSION_RE = re.compile( - r"claude[-_]" + r"(?[a-z]+)[-_](?P\d+)" r"(?:[-_.](?P\d{1,2}))?" @@ -80,7 +80,7 @@ class AnthropicProvider(LargeLanguageModel): # 无版本号的现役家族名(如 ``claude-mythos-preview``)。这些是 5 代命名, # 官方端点同样拒绝 legacy 采样控制字段。 _UNVERSIONED_MODERN_MODEL_RE = re.compile( - r"claude[-_](?:fable|mythos)(?:[-_][a-z0-9]+)*$", + r"(? Date: Mon, 21 Sep 2026 12:02:39 +0800 Subject: [PATCH 21/35] fix: describe retired base urls by their actual failure mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧 Qwen 端点的警告文案称其「已失效、会返回 404」,但 DashScope 原生 `/api/v1` 仍在服务,只是路径为 `/services/aigc/text-generation/generation`, 不提供本仓库按 OpenAI 兼容协议请求的 `{base_url}/chat/completions`。 火山旧域名 `maas-api.ml-platform-cn-beijing.volces.com` 才确实已下线。 两者失效方式不同,文案改为分别说明,避免误导用户去排查服务端故障。 同时补充文档:百炼的 `/responses` 只挂在业务空间专属域名 `{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1` 下,默认共享 域名未提供该路径,旧版 `/api/v2/apps/protocols/compatible-mode/v1/responses` 已停止维护;在默认域名上启用 `protocol = "responses"` 会收到 HTTP 404。 --- CHANGELOG.md | 2 +- docs/user_guide/llm-providers.md | 2 ++ src/utils/config.py | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20490bd..edfaa08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ - 移除 iFlow 渠道及其配置示例。 - 移除 qwen rerank 配置示例;该渠道不提供 rerank 能力,调用时会显式报错。 - 发布脚本新增目标环境校验,`--target` 与主机系统/架构不匹配时在清理构建产物前显式失败,不再生成错误架构的发布包。 -- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。旧端点已失效,配置仍为旧值时会在加载时给出显式升级警告。 +- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。配置仍为旧值时会在加载时给出显式升级警告:DashScope 原生 `api/v1` 不提供本仓库请求的 `{base_url}/chat/completions` 路径,火山旧域名 `maas-api.ml-platform-cn-beijing.volces.com` 则已停止服务。百炼的 `/responses` 只挂在业务空间专属域名下,默认共享域名不可用,文档已补充说明。 - Jina Rerank 与 SiliconFlow Rerank 一致,`return_documents` 固定为 `True` 且不允许被 `options` 覆盖。 ### 测试与文档 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index 0a7ef3d..63a64a5 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -113,6 +113,8 @@ Embedding 文档和查询会区分任务类型:Google 使用 `RETRIEVAL_DOCUME 这里的支持表示本地适配器能够发出 Responses 请求,不代表每个 Qwen、SiliconFlow、Ollama、LM Studio、DeepSeek 或 Grok 的兼容服务端都已实现 `/responses`。工厂的 `protocol_status()` 会分别返回 `adapter_supported` 与 `server_verified`;当前仓库未对这些外部 Base URL 做真实渠道验收,`server_verified` 默认是 `false`。启用前请确认目标 Base URL 的服务端文档和实际能力;不支持时应删除 `protocol` 或改回 `chat_completions`。 +注意「供应商支持 Responses」不等于「你配置的那个 Base URL 支持 Responses」。百炼(DashScope)的 `/responses` 挂在业务空间专属域名 `https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1` 下,默认的共享域名 `dashscope.aliyuncs.com/compatible-mode/v1` 并未提供该路径;百炼旧版 Responses 路径 `/api/v2/apps/protocols/compatible-mode/v1/responses` 也已停止维护。若在默认域名上启用 `protocol = "responses"`,请求会收到 HTTP 404,此时应改用专属域名或回到 `chat_completions`。 + ### 3. 定义模型实例 所有可供程序使用的模型都在 `config.toml` 中以 TOML 表定义,分为三类: diff --git a/src/utils/config.py b/src/utils/config.py index 3bb08fe..749665f 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -339,8 +339,13 @@ def warn_retired_base_urls(self) -> "Settings": ``compatible-mode/v1``,并把火山默认域名从 ``maas-api.ml-platform-cn-beijing.volces.com`` 换成 ``ark.cn-beijing.volces.com/api/v3``。旧的 ``config.toml`` 会覆盖新默认值, - 使请求在运行期收到 404/502。这里在配置加载时就明确提示如何修正,而不是让 - 用户从 404 反推原因;显式配置的自建或代理端点不受影响。 + 使请求在运行期失败。这里在配置加载时就明确提示如何修正,而不是让用户从 + 错误响应反推原因;显式配置的自建或代理端点不受影响。 + + 两处旧值的失效方式不同:DashScope 原生 ``/api/v1`` 本身仍在服务,但路径是 + ``/services/aigc/text-generation/generation``,而本项目按 OpenAI 兼容协议请求 + ``{base_url}/chat/completions``,因此该值在本项目内不可用;火山旧域名则确实 + 已下线。 只警告不阻断:旧值是可修复的配置问题而非安全边界,直接失败会让仍在使用 旧配置的部署完全无法启动。 @@ -349,18 +354,20 @@ def warn_retired_base_urls(self) -> "Settings": "qwen_base_url": ( "https://dashscope.aliyuncs.com/api/v1", "https://dashscope.aliyuncs.com/compatible-mode/v1", + "该端点不提供 {base_url}/chat/completions", ), "volc_base_url": ( "https://maas-api.ml-platform-cn-beijing.volces.com", "https://ark.cn-beijing.volces.com/api/v3", + "旧域名已停止服务", ), } - for field_name, (old_url, new_url) in retired.items(): + for field_name, (old_url, new_url, reason) in retired.items(): current = getattr(self, field_name, None) if isinstance(current, str) and current.rstrip("/") == old_url: warnings.warn( - f"{field_name} 指向已失效的旧端点 {old_url}," - f"请改为 {new_url}(旧端点会返回 404)。", + f"{field_name} 指向旧端点 {old_url}({reason})," + f"请改为 {new_url}。", UserWarning, stacklevel=2, ) From 6e1652e6cc86765bee37f8055261b97613acf33f Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 12:09:53 +0800 Subject: [PATCH 22/35] fix: replace invalid builtin embedding and rerank model ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内置兜底配置里存在几处无效 ID,新用户缺少对应配置段时会照抄到这些值: - `google` embedding 写的是裸名 `embedding-001`,Gemini API 的有效 ID 带 `gemini-` 前缀,正确值为 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役,不能作为兜底值)。 - `siliconflow` embedding/rerank 使用 `alibaba/` 命名空间,但官方模型广场 只有 `BAAI/` 前缀,不存在 `alibaba/`;rerank 现行 ID 为 `BAAI/bge-reranker-v2-m3`,没有 `bge-reranker-large`。 - `ollama` 的 `llama3` 改为官方现行 tag `llama3.1`。 同步把 config.toml.example 中火山文本向量化模型的注释改为待核实口径: `doubao-embedding-text-240715` 无法从官方来源确证(文档站为 JS 渲染, 签名接口需账号凭证才能校验模型 ID),已在注释中说明请以控制台开通管理为准, 并区分文本 embeddings.create 与多模态 multimodal_embeddings.create 两条路径。 --- CHANGELOG.md | 2 +- config.toml.example | 7 +++++-- src/utils/config.py | 9 +++++---- tests/test_config.py | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edfaa08..98c2e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ - 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 -- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表;文本向量化不要用 `doubao-embedding-vision` 这类多模态模型),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表;文本向量化不要用 `doubao-embedding-vision` 这类多模态模型),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 - 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。 - 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 diff --git a/config.toml.example b/config.toml.example index 59db7ad..29074e9 100644 --- a/config.toml.example +++ b/config.toml.example @@ -125,8 +125,11 @@ model_name = "text-embedding-v4" [embedding_configurations.volcengine] # 提供商实现标识 provider = "volcengine" -# API 实际模型名(bge-large-zh 已不在方舟模型列表中;这里走文本 embeddings.create, -# 不要用 doubao-embedding-vision 这类多模态模型) +# API 实际模型名。bge-large-zh 已不在方舟模型列表中。 +# 注意:本行未经官方来源确证——方舟文档站是 JS 渲染,且 AK/SK 签名接口需要账号 +# 凭证才能校验模型 ID。请在方舟控制台「开通管理 / 模型广场」核对当前可开通的 +# 文本向量化模型后替换。这里走文本 embeddings.create,不要填 doubao-embedding-vision +# 这类多模态模型(那需要 multimodal_embeddings.create)。 model_name = "doubao-embedding-text-240715" # ollama 向量化模型 diff --git a/src/utils/config.py b/src/utils/config.py index 749665f..16068e8 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -239,13 +239,14 @@ def model_dump_json(self, *args: Any, **kwargs: Any) -> str: # --- [MODEL_CONFIGURATIONS] --- embedding_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { + # 与 llm_configurations 同理:兜底值必须写成当前有效的官方 ID。 "local-hash": ModelDetail(provider="local-hash", model_name="local-hash-256"), - "google": ModelDetail(provider="google", model_name="embedding-001"), - "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-large-zh-v1.5"), + "google": ModelDetail(provider="google", model_name="gemini-embedding-2"), + "siliconflow": ModelDetail(provider="siliconflow", model_name="BAAI/bge-large-zh-v1.5"), "openai": ModelDetail(provider="openai", model_name="text-embedding-3-small"), }) rerank_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { - "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-reranker-large"), + "siliconflow": ModelDetail(provider="siliconflow", model_name="BAAI/bge-reranker-v2-m3"), }) llm_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { # 默认条目只作为缺失配置时的兜底;模型名保持在写就时仍可用的现行 ID, @@ -258,7 +259,7 @@ def model_dump_json(self, *args: Any, **kwargs: Any) -> str: "volcengine": ModelDetail(provider="volcengine", model_name="doubao-seed-2-0-lite-260428"), "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V3.2"), "openai": ModelDetail(provider="openai", model_name="gpt-5.6-sol"), - "ollama": ModelDetail(provider="ollama", model_name="llama3"), + "ollama": ModelDetail(provider="ollama", model_name="llama3.1"), "lm-studio": ModelDetail(provider="lm-studio", model_name="LM-Studio-Community/Meta-Llama-3-8B-Instruct-GGUF"), }) diff --git a/tests/test_config.py b/tests/test_config.py index f337336..9635d4d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -109,6 +109,22 @@ def test_settings_defaults(monkeypatch, tmp_path): assert "jina" not in settings.embedding_configurations +def test_settings_builtin_model_ids_are_current(monkeypatch, tmp_path): + """兜底默认值必须是当前有效的官方 ID,避免新用户照抄到已失效模型。""" + monkeypatch.setattr("src.utils.config.CONFIG_TOML_PATH", tmp_path / "absent.toml") + settings = Settings(_env_file=None) + + # Google 的裸名 ``embedding-001`` 不是有效 ID;退役的 ``text-embedding-004`` + # 也不能作为兜底值。 + assert settings.embedding_configurations["google"].model_name == "gemini-embedding-2" + # SiliconFlow 官方模型广场使用 ``BAAI/`` 命名空间,不存在 ``alibaba/`` 前缀; + # 现行 rerank ID 是 ``bge-reranker-v2-m3``,没有 ``bge-reranker-large``。 + assert settings.embedding_configurations["siliconflow"].model_name == "BAAI/bge-large-zh-v1.5" + assert settings.rerank_configurations["siliconflow"].model_name == "BAAI/bge-reranker-v2-m3" + # Ollama 官方库的现行 tag 是带次版本号的 ``llama3.1``。 + assert settings.llm_configurations["ollama"].model_name == "llama3.1" + + def test_settings_dump_hides_credentials_by_default(): settings = Settings(openai_api_key="sk-test", lm_studio_api_key="local-secret") From 92efa339446360123d48e8cd7983a9cc60acbd9a Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 12:21:39 +0800 Subject: [PATCH 23/35] docs: correct the volcengine embedding model lifecycle note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 firecrawl 抓到方舟官方文档后,上一轮写在 config.toml.example 的 「doubao-embedding-text-240715 未经官方来源确证」是错的:该 ID 确实存在, 方舟《文本向量化 API》文档给出的请求示例就是它。真实情况是它已进入下线流程: - 它属于 2025-12-19 启动的那一批,EOM(停止新购)2025-12-26,官方下线公告 已把它列在迁移表中,建议迁移到 doubao-embedding-vision-251215。 - 方舟当前模型列表的「向量化能力」章节只列 doubao-embedding-vision-251215 与 -250615,已无任何纯文本 embedding 模型;《文本向量化 API》文档本身位于 文档站的「下线文档归档」下。 - embedding 模型只有 EOM 阶段、不涉及 EOS,存量接入点不受影响,所以存量部署 不会被切断;但新接入点已无法创建。 因此示例配置暂不改值,改为在注释中说明真实状态与迁移目标。另需注意迁移目标 doubao-embedding-vision-251215 的官方示例走 /api/v3/embeddings/multimodal, 与本项目火山 embedding 使用的文本 embeddings.create 不是同一条路径,启用前 必须实测。 --- CHANGELOG.md | 4 ++-- config.toml.example | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98c2e12..1084c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,8 +38,8 @@ - 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 -- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表;文本向量化不要用 `doubao-embedding-vision` 这类多模态模型),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 -- 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 +- 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。火山的 `doubao-embedding-text-240715` 已于 2025-12-26 停止新购(EOM),且已不在方舟「向量化能力」模型列表中;官方下线公告给出的迁移目标是 `doubao-embedding-vision-251215`,该模型同时接受纯文本输入,但它按 `/api/v3/embeddings/multimodal` 提供,与本项目使用的文本 `embeddings.create` 路径不同,迁移前需实测。embedding 模型只有 EOM 阶段、不涉及 EOS,存量接入点不受影响,因此示例配置暂未改动该值。 - 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 ## [1.3.0] - 2026-03-20 diff --git a/config.toml.example b/config.toml.example index 29074e9..74c6a83 100644 --- a/config.toml.example +++ b/config.toml.example @@ -126,10 +126,12 @@ model_name = "text-embedding-v4" # 提供商实现标识 provider = "volcengine" # API 实际模型名。bge-large-zh 已不在方舟模型列表中。 -# 注意:本行未经官方来源确证——方舟文档站是 JS 渲染,且 AK/SK 签名接口需要账号 -# 凭证才能校验模型 ID。请在方舟控制台「开通管理 / 模型广场」核对当前可开通的 -# 文本向量化模型后替换。这里走文本 embeddings.create,不要填 doubao-embedding-vision -# 这类多模态模型(那需要 multimodal_embeddings.create)。 +# doubao-embedding-text-240715 已于 2025-12-26 停止新购(EOM),且不在方舟当前 +# 模型列表的「向量化能力」章节中;官方下线公告给出的迁移目标只有 +# doubao-embedding-vision-251215(多模态向量化模型,也接受纯文本输入)。 +# 迁移前请在方舟控制台「开通管理」确认账号可开通的向量化模型。 +# 注意:本项目的火山 embedding 走文本 embeddings.create(/api/v3/embeddings), +# 而 vision 系列官方示例走 /api/v3/embeddings/multimodal;启用前需实测确认。 model_name = "doubao-embedding-text-240715" # ollama 向量化模型 From d515ea42d5ca37c995e7650fba0e4d5814b4ff01 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 12:33:38 +0800 Subject: [PATCH 24/35] fix: refresh siliconflow chat model to a currently listed id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 firecrawl 复核各渠道官方页面后,SiliconFlow 的 `Qwen/Qwen3-8B` 已不在该 平台在售列表中——官方模型广场与定价页两个来源均无此 ID,其 Qwen 对话模型现 从 `Qwen/Qwen3.5-27B` 起步。示例配置与键名同步改为 `sf-qwen3-27b`。 同批复核确认无需改动的项: - Anthropic:`claude-sonnet-4-6` 在官方退役页为 Active(退役不早于 2027-02-17), 且是 Sonnet 3.5/3.7 与 Sonnet 4 的官方推荐替代;`claude-opus-5`、 `claude-sonnet-5`、`claude-fable-5-1` 均为 Active。采样参数弃用判定对 5 代与 4.x 全部生效、对 3.x 正确放行,无需调整正则。 - Google:`gemini-2.5-flash`/`gemini-2.5-pro` 均为 "No shutdown date announced"; `gemini-embedding-2` 正确,`text-embedding-004` 已于 2026-01-14 退役。 - OpenAI:`gpt-5.6-sol`/`gpt-5.6-terra` 是官方给出的推荐替代目标(现行), `text-embedding-3-large` 无退役公告。 - SiliconFlow embedding/rerank:`BAAI/bge-large-zh-v1.5`、`BAAI/bge-m3`、 `BAAI/bge-reranker-v2-m3` 均在售。 - Jina:`jina-reranker-v2-base-multilingual` 仍在列(官方旗舰已转为 v3.5, 但旧版未下线,示例保留)。 - Ollama:`llama3.1` 官方 tag 有效(8b/70b/405b,128K 上下文)。 - DeepSeek:`deepseek-v4-pro`/`deepseek-v4-flash` 现行。 - xAI:`grok-4.6` 现行。 --- CHANGELOG.md | 2 +- config.toml.example | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1084c3e..2c1061e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ - 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 -- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3-8B`/`deepseek-ai/DeepSeek-V3.2`,Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3.5-27B`/`deepseek-ai/DeepSeek-V3.2`(复核发现 `Qwen/Qwen3-8B` 已不在该平台在售列表,其 Qwen 对话模型现从 Qwen3.5-27B 起步),Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 - 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。火山的 `doubao-embedding-text-240715` 已于 2025-12-26 停止新购(EOM),且已不在方舟「向量化能力」模型列表中;官方下线公告给出的迁移目标是 `doubao-embedding-vision-251215`,该模型同时接受纯文本输入,但它按 `/api/v3/embeddings/multimodal` 提供,与本项目使用的文本 `embeddings.create` 路径不同,迁移前需实测。embedding 模型只有 EOM 阶段、不涉及 EOS,存量接入点不受影响,因此示例配置暂未改动该值。 - 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 diff --git a/config.toml.example b/config.toml.example index 74c6a83..b590f89 100644 --- a/config.toml.example +++ b/config.toml.example @@ -231,12 +231,13 @@ model_name = "doubao-seed-2-0-lite-260428" # 仅在目标 Ark Base URL 已实际验证 /responses 后保留此登记 # options = { server_verified_protocols = ["responses"] } -# sf-qwen3-8b 聊天模型(Qwen2-7B-Instruct 已从 SiliconFlow 模型广场下线) -[llm_configurations.sf-qwen3-8b] +# sf-qwen3-27b 聊天模型(Qwen2-7B-Instruct、Qwen/Qwen3-8B 均已不在 SiliconFlow +# 模型广场;该平台现列的 Qwen 对话模型从 Qwen3.5-27B 起步) +[llm_configurations.sf-qwen3-27b] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 -model_name = "Qwen/Qwen3-8B" +model_name = "Qwen/Qwen3.5-27B" # sf-deepseek-v3 聊天模型(meta-llama/* 已从 SiliconFlow 模型广场下线) [llm_configurations.sf-deepseek-v3] From 81d6a290f7ff711eee436103fc22ba901c8306f0 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 12:52:37 +0800 Subject: [PATCH 25/35] test: cover the active snapshot embedding compatibility guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `VectorStoreFactory._load_existing_state` 会在活动快照记录的 embedding provider/模型名与当前运行配置不一致时抛 RuntimeError,但此前没有任何测试覆盖 这条防护。它守的是数据正确性:向量空间不兼容时静默加载旧索引会产出错误检索 结果,而不是显式失败。 新增三个用例,分别覆盖 provider 不同、provider 相同但模型名不同(如 text-embedding-004 与 gemini-embedding-2 同属 google)、以及配置一致时应正常 加载。已做红绿验证:临时禁用检测逻辑后两个不匹配用例失败,还原后全部通过。 同时让 MockFaissStore 实现 load_snapshot,使匹配场景能走到实际加载路径。 --- CHANGELOG.md | 3 +- tests/retrieval/vdb/test_factory.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1061e..528fd53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,8 @@ ### 测试与文档 -- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 800。 +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 817。 +- 为活动快照的 embedding 兼容性检测补充回归测试:快照记录的 embedding provider 或模型名与当前运行配置不一致时必须显式失败,避免用错向量空间后静默产出错误检索结果。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 - 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3.5-27B`/`deepseek-ai/DeepSeek-V3.2`(复核发现 `Qwen/Qwen3-8B` 已不在该平台在售列表,其 Qwen 对话模型现从 Qwen3.5-27B 起步),Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 diff --git a/tests/retrieval/vdb/test_factory.py b/tests/retrieval/vdb/test_factory.py index ed712f5..f74c339 100644 --- a/tests/retrieval/vdb/test_factory.py +++ b/tests/retrieval/vdb/test_factory.py @@ -34,6 +34,10 @@ def save(self, path: str): def load(self, path: str): """模拟加载操作""" + def load_snapshot(self, snapshot_dir: str): + """模拟从快照目录加载,记录路径供断言。""" + self.file_path = snapshot_dir + def get_embedding_model(self) -> Any: """模拟获取嵌入模型""" return MagicMock() # 返回一个模拟的嵌入模型 @@ -120,3 +124,66 @@ def test_get_default_vector_store_without_loading_existing(): store = VectorStoreFactory.get_default_vector_store(load_existing=False) assert isinstance(store, MockFaissStore) assert store.file_path is None + +def _write_snapshot(root, snapshot_id, provider, model): + """按真实快照目录结构写入一个活动快照,供兼容性检测使用。""" + from pathlib import Path + + from src.runtime.contracts import KnowledgeSnapshotManifest + + snapshot_dir = Path(root) / snapshot_id + snapshot_dir.mkdir(parents=True, exist_ok=True) + manifest = KnowledgeSnapshotManifest.create( + snapshot_id=snapshot_id, + store_type="faiss", + embedding_provider=provider, + embedding_model=model, + chunk_mode="standard", + source_digest="test-digest", + document_count=1, + chunk_count=1, + ) + (snapshot_dir / "manifest.toml").write_text(manifest.to_toml(), encoding="utf-8") + # 快照目录校验要求这一组文件同时存在;兼容性检测发生在 load_snapshot 之前, + # 所以这里只需占位内容,不参与断言。 + for name in ("chunks.pkl", "parents.pkl", "semantic.index", "embeddings.npy", "stats.json"): + (snapshot_dir / name).write_bytes(b"") + (Path(root) / "ACTIVE_SNAPSHOT").write_text(snapshot_id, encoding="utf-8") + return snapshot_dir + + +def test_default_vector_store_rejects_mismatched_embedding_provider(tmp_path): + """活动快照的 embedding provider 与当前运行配置不同时必须显式失败。 + + 向量空间不兼容时静默加载会产出错误的检索结果,因此这里要求抛出 + 带快照值与当前值的 RuntimeError,而不是继续使用旧索引。 + """ + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-mismatch", "openai", "text-embedding-3-large") + + with pytest.raises(RuntimeError, match="embedding 配置与当前运行配置不一致"): + VectorStoreFactory.get_default_vector_store() + + +def test_default_vector_store_rejects_mismatched_embedding_model(tmp_path): + """provider 相同但模型名不同同样不兼容,也必须拦截。""" + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-model-mismatch", "local-hash", "local-hash-512") + + with pytest.raises(RuntimeError, match="embedding 配置与当前运行配置不一致"): + VectorStoreFactory.get_default_vector_store() + + +def test_default_vector_store_accepts_matching_embedding(tmp_path): + """配置一致时正常加载,不应触发兼容性错误。""" + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-match", "local-hash", "local-hash-256") + + store = VectorStoreFactory.get_default_vector_store() + assert isinstance(store, MockFaissStore) From d7d8be8ad314b1bd6083dad60a4d5b2c6b27607a Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 15:01:25 +0800 Subject: [PATCH 26/35] fix: redact masked credential shapes in server error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 服务端返回的凭证错误常带「首尾可见、中间掩码」的形态,例如 `sk-abc***...***xyz`。`_OPENAI_KEY_RE` 要求 `sk-` 之后连续 8 个以上字母数字, 星号会中断匹配,于是整串原样落进日志。 新增 `_MASKED_CREDENTIAL_RE` 单独覆盖这一形态,并在 `redact_sensitive_text` 中于 `_OPENAI_KEY_RE` 之前应用。已做红绿验证:移除该规则后两个掩码用例失败, 还原后通过。 --- CHANGELOG.md | 4 ++-- src/utils/security.py | 7 +++++++ tests/providers/test_security_boundaries.py | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528fd53..5ef9309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - 新增配置、请求与日志边界的凭证校验:模型级 `options` 与请求级扩展不得携带密钥、请求头、query 或 Base URL 覆盖,资源 Facade 的关键字与位置参数同样受检。 - 凭证键识别补齐火山引擎的 `ak`/`sk` 与 Azure 存储的 `account_key`;此前这些键可绕过请求覆盖校验,而 `api_key` 会被拒绝。 -- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query);补齐 `ak=`/`sk=`/`account_key=` 形式。 +- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query);补齐 `ak=`/`sk=`/`account_key=` 形式,以及服务端错误里「首尾可见、中间掩码」的凭证形态(如 `sk-abc***...***xyz`,此前会原样落进日志)。 - 修复脱敏日志 formatter 的缓存泄漏:`logging.Formatter` 会把格式化后的 traceback 缓存在 `record.exc_text` 上供后续 handler 复用,先格式化后脱敏会让同一 logger 上的非脱敏 handler 输出原始凭证;现在脱敏结果会写回缓存。 - 兼容渠道不再被当作官方 OpenAI 端点放行 SDK 专属资源;仅精确匹配 `https://api.openai.com/v1` 时按 SDK 契约放行。 - 收紧失败语义:凭证、SDK 或远端调用失败时显式报错,不再以空 embedding、零分 rerank 或占位回答掩盖失败。 @@ -35,7 +35,7 @@ ### 测试与文档 -- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 817。 +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 820。 - 为活动快照的 embedding 兼容性检测补充回归测试:快照记录的 embedding provider 或模型名与当前运行配置不一致时必须显式失败,避免用错向量空间后静默产出错误检索结果。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 diff --git a/src/utils/security.py b/src/utils/security.py index e3a1937..57297a8 100644 --- a/src/utils/security.py +++ b/src/utils/security.py @@ -117,6 +117,12 @@ r'''(?:"[^"]*"|'[^']*'|[^\s,;}']+)''' ) _OPENAI_KEY_RE = re.compile(r"\b(?:sk|rk|sess)-[A-Za-z0-9_-]{8,}\b", re.IGNORECASE) +# 服务端返回的错误信息常带「首尾可见、中间掩码」的凭证形态,例如 +# ``sk-abc***...***xyz``。``_OPENAI_KEY_RE`` 要求 ``sk-`` 后连续 8 个以上 +# 字母数字,星号会中断匹配,于是整串原样落进日志。这里单独覆盖掩码形态。 +_MASKED_CREDENTIAL_RE = re.compile( + r"(?i)\b(?:sk|rk|sess)-[A-Za-z0-9_-]{2,}\*{3,}[A-Za-z0-9_-]*" +) _GOOGLE_API_KEY_RE = re.compile(r"\bAIza[0-9A-Za-z_-]{20,}\b") @@ -129,6 +135,7 @@ def redact_sensitive_text(value: Any) -> str: redacted = _SHORT_CREDENTIAL_KEY_RE.sub(r"\1=[REDACTED]", redacted) redacted = _URL_USERINFO_RE.sub(r"\1[REDACTED]:[REDACTED]@", redacted) redacted = _URL_QUERY_SECRET_RE.sub(r"\1[REDACTED]", redacted) + redacted = _MASKED_CREDENTIAL_RE.sub("[REDACTED]", redacted) redacted = _OPENAI_KEY_RE.sub("[REDACTED]", redacted) return _GOOGLE_API_KEY_RE.sub("[REDACTED]", redacted) diff --git a/tests/providers/test_security_boundaries.py b/tests/providers/test_security_boundaries.py index 7c58582..ef81b8c 100644 --- a/tests/providers/test_security_boundaries.py +++ b/tests/providers/test_security_boundaries.py @@ -50,3 +50,24 @@ def test_security_redacts_short_credential_key_assignments(value): def test_security_does_not_redact_ordinary_text_with_ak_or_sk(value): """普通文本里的 ``sk``/``ak`` 子串不能被当成凭证脱敏。""" assert redact_sensitive_text(value) == value + + +@pytest.mark.parametrize( + "value", + [ + "Incorrect API key provided: sk-abc***...***xyz.", + "Invalid token: rk-abc*******************def", + "key sess-abcd****wxyz rejected", + ], +) +def test_security_redacts_masked_credential_shapes(value): + """服务端错误里「首尾可见、中间掩码」的凭证形态也要脱敏。 + + ``_OPENAI_KEY_RE`` 要求 ``sk-`` 后连续 8 个以上字母数字,星号会中断匹配, + 这类字符串此前会原样落进日志。 + """ + redacted = redact_sensitive_text(value) + assert "[REDACTED]" in redacted + assert "sk-abc" not in redacted + assert "rk-abc" not in redacted + assert "sess-abcd" not in redacted From dd29972431707e6edc1f8decd075ad3678caa97a Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 16:26:26 +0800 Subject: [PATCH 27/35] fix: reject ark instructions with enabled caching at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 官方 Ark Responses 文档明确 `instructions` 不可与缓存能力一起使用,且 `caching` 配置为 `{"type": "enabled"}` 时请求会直接报错。SDK 不做本地校验, 会原样发到服务端,用户只能从远端错误反推原因。 `_build_responses_request` 此前允许两者同时出现:`instructions` 来自 `system_prompt`,而 `CompletionRequest.system_prompt` 带兼容默认值 `"You are a helpful assistant."`,因此只要用 `prompt=` 且传了 `caching` 就会命中。现在在构造阶段显式拒绝,错误信息点明默认值来源并给出修复方式 (显式传 `system_prompt=None` 并把系统提示放进 `messages`,或移除 `caching`)。 已做红绿验证:移除校验后对应用例失败,还原后通过。 --- CHANGELOG.md | 3 +- src/providers/volcengine.py | 15 ++++++ tests/providers/test_sdk_capabilities.py | 66 ++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef9309..bc855b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - 修复多轮工具调用的历史归一化:共享的 `normalize_messages` 不再强制要求工具调用带 `id`,因为 Gemini 的 `FunctionCall.id` 是可选字段且 SDK 默认 `None`,否则把上一轮工具调用回填给 Google 会在请求发出前被本地拒绝;需要 `id` 的 Chat Completions 与 Responses 改为显式校验。同一处还修复了 `input` 参数别名被静默清空,以及 `function_call`/`custom_tool_call` 类型未映射为 `function` 的问题。 - 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类线协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses,Ark 的 `ark` 取值是 Chat Completions 的别名。 - Responses 对非官方端点要求 `options.server_verified_protocols = ["responses"]` 显式登记;未登记时工厂、Provider 和资源 Facade 均在请求前拒绝,避免把本地 SDK 资源误发到未实现 `/responses` 的服务端。 +- Ark Responses 在构造阶段拒绝 `instructions` 与 `caching={"type": "enabled"}` 同时出现:官方规定配置 `instructions` 后本轮请求无法写入或使用缓存,`caching` 为 `enabled` 时服务端直接报错,而 SDK 不做本地校验。错误信息会指出 `instructions` 来自 `system_prompt` 的兼容默认值,并给出改走 `messages` 的修复方式。 - 新增原生资源 Facade,显式暴露文件、批处理、缓存、向量库、token 计数、调优等 SDK 能力,不再把资源生命周期混入普通聊天请求。 - 深度适配 Google GenAI、Anthropic、Volcengine Ark、Jina 与 SiliconFlow Rerank;OpenAI 兼容渠道复用 `openai` SDK,Jina 与 SiliconFlow Rerank 使用 HTTP JSON。 - Embedding 区分文档与查询任务类型:Google 使用 `RETRIEVAL_DOCUMENT` 与 `RETRIEVAL_QUERY`。 @@ -35,7 +36,7 @@ ### 测试与文档 -- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 820。 +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 823。 - 为活动快照的 embedding 兼容性检测补充回归测试:快照记录的 embedding provider 或模型名与当前运行配置不一致时必须显式失败,避免用错向量空间后静默产出错误检索结果。 - 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 - 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 diff --git a/src/providers/volcengine.py b/src/providers/volcengine.py index a5a6520..d6bfa08 100644 --- a/src/providers/volcengine.py +++ b/src/providers/volcengine.py @@ -912,6 +912,21 @@ def _build_responses_request(self, request: CompletionRequest) -> dict[str, Any] "Ark Responses", set(params).difference({"extra_body"}), ) + # 官方文档明确:``instructions`` 不可与缓存能力一起使用,``caching`` 配置为 + # ``{"type": "enabled"}`` 时请求会直接报错。SDK 不做本地校验,会原样发到 + # 服务端,因此在构造阶段显式拒绝,避免用户从远端错误反推原因。 + if params.get("instructions") is not None: + enabled_caching = params.get("caching") + if isinstance(enabled_caching, Mapping) and enabled_caching.get("type") == "enabled": + raise ValueError( + "Ark Responses 的 instructions 与 caching={\"type\": \"enabled\"} 互斥:" + "官方规定配置 instructions 后本轮请求无法写入或使用缓存,caching 为 " + "enabled 时请求会直接报错。instructions 来自 system_prompt," + "注意 CompletionRequest.system_prompt 有兼容默认值" + "(未显式传入时为 \"You are a helpful assistant.\");" + "请显式传入 system_prompt=None 并改用 messages 携带系统提示," + "或移除 caching。" + ) return params @classmethod diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py index 53d6933..6870baf 100644 --- a/tests/providers/test_sdk_capabilities.py +++ b/tests/providers/test_sdk_capabilities.py @@ -749,6 +749,72 @@ def test_openai_chat_explicit_extra_body_extensions_cannot_be_silently_overridde provider._build_chat_request(prompt="hi", stream=False, **kwargs) +def test_ark_responses_rejects_instructions_with_enabled_caching(): + """官方规定 ``instructions`` 与 ``caching={"type": "enabled"}`` 互斥。 + + 配置 instructions 后本轮请求无法写入或使用缓存,caching 为 enabled 时 + 服务端会直接报错;SDK 不做本地校验,因此这里要求在构造阶段显式失败。 + """ + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + with pytest.raises(ValueError, match="instructions.*caching"): + provider._build_responses_request( + CompletionRequest( + prompt="hi", + system_prompt="你是助手", + caching={"type": "enabled"}, + stream=False, + ) + ) + + +def test_ark_responses_accepts_system_prompt_without_enabled_caching(): + """未启用 caching 时 instructions 正常透传;caching 非 enabled 也不拦截。""" + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + params = provider._build_responses_request( + CompletionRequest(prompt="hi", system_prompt="你是助手", stream=False) + ) + assert params["instructions"] == "你是助手" + assert "caching" not in params + + params = provider._build_responses_request( + CompletionRequest( + prompt="hi", system_prompt="你是助手", caching={"type": "disabled"}, stream=False + ) + ) + assert params["instructions"] == "你是助手" + assert params["caching"] == {"type": "disabled"} + + +def test_ark_responses_enabled_caching_ok_without_instructions(): + """把系统提示放进 messages 并显式关闭 system_prompt 时,caching 可正常启用。""" + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + params = provider._build_responses_request( + CompletionRequest( + messages=[ + {"role": "system", "content": "你是助手"}, + {"role": "user", "content": "hi"}, + ], + system_prompt=None, + caching={"type": "enabled"}, + stream=False, + ) + ) + assert "instructions" not in params + assert params["caching"] == {"type": "enabled"} + + @pytest.mark.parametrize("field", ["top_k", "seed", "stop", "reasoning"]) def test_openai_responses_explicit_extra_body_extensions_cannot_be_silently_overridden(field): provider = object.__new__(OpenAICompatibleProvider) From e4e5b1c9fdb3af21b1084c95bb70dc34c9a8de79 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 18:05:25 +0800 Subject: [PATCH 28/35] chore: remove dead code left by inlined logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 AST 扫描全部 52 个源文件的 1519 个函数/方法定义,逐个核对引用后删除以下 死代码。它们的共同特征是同一行为已有另一处实现: - `has_system_message`:逻辑已在 `normalize_messages` 内联 (`any(message.get("role") == "system" ...)`)。 - `_configured_extra_body_keys`:`set(configured_extra_body)` 在 `_build_chat_request` 与 `_build_responses_request` 各内联一次。 - `SnapshotRepository.write_stats` / `load_stats`:`stats.json` 实际由 `FaissStore.save_snapshot` 内联写入,且全项目从未读取该文件。 - `SnapshotRepository.load_active_manifest`:`get_active_snapshot_dir` + `validate_snapshot_dir` + `load_manifest` 的组合封装,无调用点。 - `recursive_text_splitter` 的 `DEFAULT_CHILD_CHUNK_SIZE` / `DEFAULT_CHILD_CHUNK_OVERLAP`:`__init__` 用 `None` 默认值并回退到 `Settings`,这两个常量未被引用,且与 `config.py` 的默认值重复。 保留了由框架调用的函数(pydantic `@field_validator`、自定义 `PydanticBaseSettingsSource.get_field_value`)和公开迁移入口 (`FaissStore.import_legacy_snapshot`),它们无显式调用点但并非死代码。 净删除 37 行,无新增;823 项测试保持通过。 --- CHANGELOG.md | 1 + src/etl/splitters/recursive_text_splitter.py | 3 --- src/providers/__base__/model_provider.py | 4 ---- src/providers/openai_compatible.py | 9 --------- src/retrieval/snapshot_repository.py | 21 -------------------- 5 files changed, 1 insertion(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc855b9..a880a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ ### 移除与调整 - 移除 Dify 上游子模块;`src/etl/`、`src/retrieval/` 等移植代码继续遵守 `DIFY_LICENSE` 并保留上游版权声明。 +- 清理死代码:移除 `has_system_message`、`_configured_extra_body_keys`、`SnapshotRepository.write_stats`/`load_stats`/`load_active_manifest`,以及 `recursive_text_splitter` 中未被引用的 `DEFAULT_CHILD_CHUNK_*` 常量。这些逻辑均已在调用点内联或由框架通过装饰器调用,保留会造成同一行为的两处实现。 - 移除 iFlow 渠道及其配置示例。 - 移除 qwen rerank 配置示例;该渠道不提供 rerank 能力,调用时会显式报错。 - 发布脚本新增目标环境校验,`--target` 与主机系统/架构不匹配时在清理构建产物前显式失败,不再生成错误架构的发布包。 diff --git a/src/etl/splitters/recursive_text_splitter.py b/src/etl/splitters/recursive_text_splitter.py index f61ac75..1828d98 100644 --- a/src/etl/splitters/recursive_text_splitter.py +++ b/src/etl/splitters/recursive_text_splitter.py @@ -13,9 +13,6 @@ logger = get_module_logger(__name__) -DEFAULT_CHILD_CHUNK_SIZE = 300 -DEFAULT_CHILD_CHUNK_OVERLAP = 30 - class RecursiveTextSplitter(BaseSplitter): """ 递归文本分割器。 diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 50ed5fd..8fa2bdc 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -967,10 +967,6 @@ def normalize_responses_input( return converted -def has_system_message(messages: Sequence[Mapping[str, Any]] | None) -> bool: - """判断调用方是否已经在消息列表中提供 system 消息。""" - return bool(messages and any(message.get("role") == "system" for message in messages)) - def normalize_usage(usage: Any) -> dict[str, Any]: """将常见供应商 usage 结构规范为统一 token 字段。""" diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py index e293ebd..c7e8974 100644 --- a/src/providers/openai_compatible.py +++ b/src/providers/openai_compatible.py @@ -438,15 +438,6 @@ async def _poll_background_response_async(self, response: Any, params: Mapping[s status = self._field(response, "status") return response - def _configured_extra_body_keys(self) -> set[str]: - """返回模型级 extra_body 的显式字段名,用于冲突检测。""" - configured = (getattr(self, "_options", {}) or {}).get("extra_body") - if configured is None: - return set() - if not isinstance(configured, Mapping): - raise ValueError("Provider options.extra_body 必须是对象。") - return set(configured) - def _compat_extra_options(self) -> dict[str, Any]: return { key: value diff --git a/src/retrieval/snapshot_repository.py b/src/retrieval/snapshot_repository.py index c7ae969..4fa9774 100644 --- a/src/retrieval/snapshot_repository.py +++ b/src/retrieval/snapshot_repository.py @@ -1,11 +1,9 @@ from __future__ import annotations -import json import shutil import tomllib import uuid from pathlib import Path -from typing import Any from src.runtime.contracts import KnowledgeSnapshotManifest, RunConfig @@ -69,18 +67,6 @@ def load_manifest(self, snapshot_dir: Path) -> KnowledgeSnapshotManifest: data = tomllib.load(file) return KnowledgeSnapshotManifest.from_mapping(data) - def write_stats(self, snapshot_dir: Path, stats: dict[str, Any]) -> None: - (snapshot_dir / "stats.json").write_text( - json.dumps(stats, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - - def load_stats(self, snapshot_dir: Path) -> dict[str, Any]: - stats_path = snapshot_dir / "stats.json" - if not stats_path.exists(): - return {} - return json.loads(stats_path.read_text(encoding="utf-8")) - def validate_snapshot_dir(self, snapshot_dir: Path) -> None: required_files = [ snapshot_dir / "manifest.toml", @@ -93,10 +79,3 @@ def validate_snapshot_dir(self, snapshot_dir: Path) -> None: missing_files = [str(path.name) for path in required_files if not path.exists()] if missing_files: raise FileNotFoundError(f"知识快照不完整,缺少文件: {', '.join(missing_files)}") - - def load_active_manifest(self) -> KnowledgeSnapshotManifest | None: - snapshot_dir = self.get_active_snapshot_dir() - if snapshot_dir is None: - return None - self.validate_snapshot_dir(snapshot_dir) - return self.load_manifest(snapshot_dir) From e4264bc7129f97d3dacaab1e46e60e81a9d912e5 Mon Sep 17 00:00:00 2001 From: Mison Date: Mon, 21 Sep 2026 19:38:59 +0800 Subject: [PATCH 29/35] docs: document base url override, snapshot guard and model lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充三类此前只在代码里存在的约束: - `.env` 的 `OPENAI_API_BASE` 会覆盖 `config.toml` 的 `openai_api_base`,指向网关时 `model_name` 必须是网关实际提供的 ID。 - 旧端点升级警告的实际失效方式:DashScope 原生 `/api/v1` 仍在服务但不提供 `{base_url}/chat/completions`;火山旧域名已下线。 - 活动快照的 embedding 兼容性硬检查,以及切换模型后需重建快照的原因。 - 模型 ID 退役表、`kimi-k3` 的 temperature=1 限制、Ark `instructions` 与 `caching` 互斥, 以及火山 embedding 与百炼 Responses 域名的已知问题。 --- docs/user_guide/configuration.md | 30 +++++++++++++++++++ docs/user_guide/llm-providers.md | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index 0659aa7..bd8b69b 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -73,6 +73,22 @@ OPENAI_API_BASE="https://api.openai.com/v1" DEFAULT_LLM_PROVIDER="openai" ``` +`.env` 中的 `OPENAI_API_BASE` 会覆盖 `config.toml` 的 `openai_api_base`,适合把流量指向代理、中转或自建网关。 +注意 `provider = "openai"` 的条目都会走这个 Base URL,因此指向网关时,`model_name` 必须是该网关实际提供的模型 ID。 + +### 旧端点升级提示 + +配置加载时会对已知失效的旧端点发出显式警告,而不是让请求在运行期失败: + +- `qwen_base_url` 为 `https://dashscope.aliyuncs.com/api/v1` 时会提示改为 `compatible-mode/v1`。 + DashScope 原生 `/api/v1` 本身仍在服务,但它的路径是 + `/services/aigc/text-generation/generation`,不提供本项目按 OpenAI 兼容协议请求的 + `{base_url}/chat/completions`。 +- `volc_base_url` 为 `https://maas-api.ml-platform-cn-beijing.volces.com` 时会提示改为 + `https://ark.cn-beijing.volces.com/api/v3`;旧域名已停止服务。 + +自建或代理端点不受影响,警告只针对上述精确匹配的旧值。 + ## 主要字段 ### Base URL @@ -135,3 +151,17 @@ model_name = "local-hash-256" ``` 它用于本地可复现验证,不依赖外部 Embedding API。 + +### 切换 Embedding 需重建快照 + +活动快照的 `manifest.toml` 会记录构建时的 `embedding_provider` 与 `embedding_model`。 +加载快照时框架会核对两者是否与当前运行配置一致,不一致会显式报错: + +``` +活动知识快照的 embedding 配置与当前运行配置不一致: +快照=/,当前=/。请重建知识快照或切换回原 embedding 配置。 +``` + +这是刻意的硬性检查:不同模型的向量空间不兼容,静默复用旧索引会产出错误的检索结果。 +切换 Embedding 模型(包括同一 provider 内换模型,例如 `text-embedding-004` → `gemini-embedding-2`) +后必须重建知识快照。 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index 63a64a5..8dad468 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -24,6 +24,19 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 * **SiliconFlow**: 一个集成了多种开源模型的平台,可通过其统一 API 调用。 * **Jina AI**: 主要用于提供高质量的 Rerank 模型。 +> **火山引擎向量化的已知问题**:示例配置中的 `doubao-embedding-text-240715` 已于 +> 2025-12-26 停止新购(EOM),且不在方舟「向量化能力」模型列表中。方舟官方给出的迁移目标是 +> `doubao-embedding-vision-251215`,但该项目走 `/api/v3/embeddings/multimodal`,与本项目火山 +> embedding 使用的文本 `embeddings.create` 不是同一条路径,**迁移前需要实测确认**。 +> embedding 模型只有 EOM 阶段、不涉及 EOS,已有接入点不受影响,因此示例配置暂未改动该值。 +> 默认的知识库链路使用离线 `local-hash`,不受此问题影响。 + +> **百炼的 Responses 端点挂在专属域名下**:`/responses` 只存在于业务空间专属域名 +> `https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1`,默认共享域名 +> `dashscope.aliyuncs.com/compatible-mode/v1` 并未提供该路径;旧版 +> `/api/v2/apps/protocols/compatible-mode/v1/responses` 已停止维护。在默认域名上启用 +> `protocol = "responses"` 会收到 HTTP 404。 + ### 本地化模型 * **Ollama**: 支持通过 Ollama 在本地运行的各种开源模型,如 Llama 3.1、Gemma 3 等。 @@ -182,3 +195,40 @@ model_name = "gpt-5.6-sol" 3. **重启程序**: 保存 `config.toml` 文件并重新启动 `uv run main.py`。 现在,您就可以在 `/config` 菜单的“切换模型” -> “语言模型 (LLM)” 选项中看到并选择 `sf-glm-5-3` 了。 + +### 模型 ID 会退役,示例值不等于长期有效 + +各厂商会定期下线旧模型 ID,届时调用会直接失败(通常表现为 404 或 503 `model_not_found`), +错误来自服务端而非本项目。示例配置中的模型 ID 是写入时仍可用的值,长期使用前请在对应平台的 +模型列表中核对。 + +已知的退役规律与替代项见 `CHANGELOG.md` 的 `[1.4.0]` 节;概览: + +| 渠道 | 已退役 | 现行替代 | +| --- | --- | --- | +| Google | `text-embedding-004` | `gemini-embedding-2`(向量空间不兼容,需重建快照) | +| OpenAI | `gpt-4o`、`gpt-3.5-turbo` | `gpt-5.6-sol` / `gpt-5.6-terra` | +| Anthropic | `claude-3-5-sonnet-20240620` | `claude-sonnet-4-6` | +| DeepSeek | `deepseek-chat` | `deepseek-v4-pro` / `deepseek-v4-flash` | +| 火山方舟 | `doubao-pro-32k`、`bge-large-zh` | `doubao-seed-2-0-lite-260428`;向量化见下方说明 | +| SiliconFlow | `Qwen/Qwen2-7B-Instruct`、`Qwen/Qwen3-8B` | `Qwen/Qwen3.5-27B` | + +### 部分模型有专属参数限制 + +个别模型对请求字段有额外约束,本项目会在构造请求前显式报错,而不是把非法请求发到服务端: + +- **`kimi-k3`** 只接受 `temperature = 1`。项目默认 `chat_temperature = 0.7` 会触发 HTTP 400, + 需在该模型条目中覆盖: + + ```toml + [llm_configurations.kimi-k3] + provider = "openai" + model_name = "kimi-k3" + options = { temperature = 1 } + ``` + +- **Ark Responses 的 `instructions` 与 `caching` 互斥**:官方规定配置 `instructions` 后本轮请求 + 无法写入或使用缓存,`caching = {"type": "enabled"}` 时服务端直接报错。注意 + `CompletionRequest.system_prompt` 带兼容默认值,未显式传入时也会设置 `instructions`; + 如需两者并存,请显式传 `system_prompt=None` 并把系统提示放进 `messages`。 + From 7a60a0a7bc294fb38d74a5624406d6325fd27c01 Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 01:53:33 +0800 Subject: [PATCH 30/35] fix: close four credential-boundary gaps in redaction and scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 脱敏正则与 is_sensitive_option_key 用了两套键名词表:client_secret、 private_key、credentials、clientsecret 在配置边界被判为凭证,却因文本 正则词表更小而明文落进日志与错误信息。改为由单一 _TEXT_CREDENTIAL_KEYS 派生全部文本正则,消除「同一概念两处实现」的根因。 2. 掩码凭证规则排在 Bearer 规则之后:``_BEARER_TEXT_RE`` 先吃掉 ``Bearer sk-abc`` 的可见前缀,使 ``sk-`` 锚点失效,``***...***xyz`` 原样残留。改为掩码优先,并放宽字符类以覆盖 ``.`` 与 ``…`` 占位符。 3. ``_url_credential_paths`` 在 urlsplit 抛 ValueError 时直接 return []。 该分支是叶子值的唯一检查入口,于是任何凭证只要拼在一个畸形 URL 片段 (如 ``https://[::1``)后面就能整体绕过边界校验。改为退回文本脱敏判定。 4. 新增共用的 safe_exception_text:终端与日志是凭证最易泄漏的出口, SDK 异常常回显请求头与 URL,各调用点此前各自处理且不一致。 Co-Authored-By: Claude Opus 5 (1M context) --- src/utils/security.py | 85 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/src/utils/security.py b/src/utils/security.py index 57297a8..9d2d25f 100644 --- a/src/utils/security.py +++ b/src/utils/security.py @@ -96,17 +96,64 @@ # word as a credential causes ordinary text (``token usage``) to be rejected by # ``find_sensitive_option_paths``. Require a credential-like value for that # form: at least twelve token characters and one digit or token punctuation. +# 文本脱敏与 is_sensitive_option_key 必须使用同一套凭证键名,否则会出现 +# 「配置边界判为敏感、日志里却明文输出」的漏洞(client_secret、private_key、 +# credentials 都曾如此)。下面显式列出可在文本中安全识别的键名,所有文本正则 +# 均由它派生;``[_-]?`` 同时覆盖 ``client_secret`` 与 ``clientsecret``。 +# +# 不含 ak/sk:这两个键太短,需要独立的词边界正则(见 _SHORT_CREDENTIAL_KEY_RE), +# 否则 ``task = value`` 里的 sk 会被误脱敏。 +# 也不含 base_url / client_args / http_client 等连接类键名:它们在配置边界被禁止 +# 是因为会覆盖请求边界,本身不是凭证;纳入文本识别会让普通配置值被误判为凭证。 +_TEXT_CREDENTIAL_KEYS = ( + "secret[_-]?access[_-]?key", + "ssh[_-]?private[_-]?key", + "proxy[_-]?authorization", + "secret[_-]?credential", + "workload[_-]?identity", + "x[_-]?access[_-]?token", + "x[_-]?auth[_-]?token", + "x[_-]?api[_-]?key", + "admin[_-]?api[_-]?key", + "encryption[_-]?key", + "signing[_-]?key", + "private[_-]?key", + "client[_-]?secret", + "secret[_-]?value", + "webhook[_-]?secret", + "account[_-]?key", + "access[_-]?token", + "auth[_-]?token", + "access[_-]?key", + "secret[_-]?key", + "auth[_-]?key", + "set[_-]?cookie", + "google[_-]?key", + "goog[_-]?key", + "ssh[_-]?key", + "api[_-]?key", + "authorization", + "credential", + "credentials", + "password", + "passwd", + "cookie", + "secret", + "token", + "auth", + "bearer", +) +_TEXT_CREDENTIAL_KEY_PATTERN = "|".join(_TEXT_CREDENTIAL_KEYS) + _KEY_VALUE_TEXT_RE = re.compile( - r'''(?i)(api[_-]?key|access[_-]?key|secret[_-]?key|authorization|token|password)["']?''' + r"(?i)(? str: """脱敏异常、日志和请求错误中的常见凭证表示。""" if not isinstance(value, str): value = str(value) - redacted = _BEARER_TEXT_RE.sub("Bearer [REDACTED]", value) + # 掩码形态必须最先处理:``_BEARER_TEXT_RE`` 会先吃掉 ``Bearer sk-abc`` + # 的可见前缀,使后续的 ``sk-`` 锚点失效,尾部掩码片段就会残留。 + redacted = _MASKED_CREDENTIAL_RE.sub("[REDACTED]", value) + redacted = _BEARER_TEXT_RE.sub("Bearer [REDACTED]", redacted) redacted = _KEY_VALUE_TEXT_RE.sub(r"\1=[REDACTED]", redacted) redacted = _SHORT_CREDENTIAL_KEY_RE.sub(r"\1=[REDACTED]", redacted) redacted = _URL_USERINFO_RE.sub(r"\1[REDACTED]:[REDACTED]@", redacted) redacted = _URL_QUERY_SECRET_RE.sub(r"\1[REDACTED]", redacted) - redacted = _MASKED_CREDENTIAL_RE.sub("[REDACTED]", redacted) redacted = _OPENAI_KEY_RE.sub("[REDACTED]", redacted) return _GOOGLE_API_KEY_RE.sub("[REDACTED]", redacted) + +def safe_exception_text(exc: BaseException) -> str: + """把异常转成可安全展示的文本:脱敏凭证,并限制长度。 + + 终端与日志是凭证最容易泄漏的出口——SDK 的异常消息常回显请求头或 + URL。所有面向用户的异常输出都应经过这里,而不是直接 ``str(exc)`` + 或把异常对象交给 ``console.print`` / ``logger.error``。 + """ + message = str(exc).strip() + if not message: + return type(exc).__name__ + return f"{type(exc).__name__}: {redact_sensitive_text(message)[:240]}" + + def _copy_nested(value: Any) -> Any: """递归复制配置容器,尽量隔离可变的第三方 SDK 对象。""" if isinstance(value, Mapping): @@ -240,7 +305,11 @@ def _url_credential_paths(value: Any, path: str) -> list[str]: try: parsed = urlsplit(value) except ValueError: - return [] + # urlsplit 对畸形 URL(例如 ``https://[::1``)抛 ValueError。这不是 + # 「值不是 URL」而是「无法解析」,不能就此放弃检查:该分支是叶子值的 + # 唯一入口,直接 return 会让任意凭证随一个畸形前缀整体绕过边界校验。 + # 退回文本脱敏判定,保持与正常 URL 路径一致的严格度。 + return [path or "value"] if redact_sensitive_text(value) != value else [] found: list[str] = [] if redact_sensitive_text(value) != value: found.append(path or "value") From 3b9119450395193f023b612506456f7d3a9daa17 Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 01:56:15 +0800 Subject: [PATCH 31/35] fix: enforce Ark responses guards on every construction path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Ark Responses 非流式入口读取 ``response.output_text``,但 Ark SDK 的 ``Response`` 没有该字段(OpenAI SDK 才把它实现为聚合 property),正文位于 ``output[].content[].text``。于是 complete / acomplete / invoke(stream=False) / ainvoke(stream=False) 全部返回空文本且不报错,ChatService.identify_intent 随后抛出「意图识别返回空结果」。改为从 output 内容块提取正文,并在标记 completed 却无正文、工具调用、拒答与推理时显式失败,不再用空结果掩盖。 2. server_verified_protocols 门禁只在显式别名上实现,动态资源树 (``resources.responses.create(...)``)只做凭证校验即可发出请求。改为在 ``_NativeResourceProxy.__call__`` 复用 provider 的统一钩子,两条路径共用 同一不变量;传完整路径而非末段方法名,避免误拦 ``files.create``。 3. instructions 与 caching={"type":"enabled"} 的互斥检查只看顶层 ``caching``, 漏掉 ``extra_body``(Ark SDK 会把它合并进请求体)与原生 kwargs 两条路径。 抽出 ``_reject_instructions_with_enabled_caching`` 并覆盖全部来源。 4. 凭证校验提到能力门禁之前:否则携带凭证的调用会报出能力错误,把安全问题 掩盖成配置问题。 Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/__base__/model_provider.py | 15 ++-- src/providers/resources.py | 12 ++++ src/providers/volcengine.py | 87 ++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 8fa2bdc..c48ccaa 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -1494,12 +1494,14 @@ def _guard_provider_resource_method(method: Callable[..., Any]) -> Callable[..., @wraps(method) async def async_wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: - resource_guard = getattr(provider, "_require_provider_resource", None) - if callable(resource_guard): - resource_guard(method.__name__) + # 凭证校验先行:它约束参数本身,与渠道是否具备该能力无关。 + # 顺序反了会让携带凭证的调用报出能力错误,把安全问题掩盖成配置问题。 safe_args, safe_kwargs = _sanitize_resource_call( method, provider, args, kwargs ) + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) provider_call_is_valid = _provider_call_accepts_arguments( method, provider, safe_args, safe_kwargs ) @@ -1525,12 +1527,13 @@ async def async_wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: @wraps(method) def wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: - resource_guard = getattr(provider, "_require_provider_resource", None) - if callable(resource_guard): - resource_guard(method.__name__) + # 与 async_wrapper 保持同一顺序:先凭证,后能力。 safe_args, safe_kwargs = _sanitize_resource_call( method, provider, args, kwargs ) + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) provider_call_is_valid = _provider_call_accepts_arguments( method, provider, safe_args, safe_kwargs ) diff --git a/src/providers/resources.py b/src/providers/resources.py index 472d908..2523705 100644 --- a/src/providers/resources.py +++ b/src/providers/resources.py @@ -62,6 +62,9 @@ def __getattr__(self, name: str) -> Any: return wrapped def __call__(self, *args: Any, **kwargs: Any) -> Any: + # 先做凭证校验:它约束的是「参数能不能出现在请求里」,与渠道是否具备 + # 该能力无关,也是更根本的边界。顺序反了会让携带凭证的调用报出能力 + # 错误,把真正的安全问题掩盖成配置问题。 safe_args = validate_secret_free_resource_args( args, f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", @@ -70,6 +73,15 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: kwargs, f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", ) + # 再做能力门禁:动态访问的资源节点必须和显式 Facade 方法受同一套约束。 + # 只做凭证校验不够:``resources.responses.create(...)`` 这类路径会绕过 + # server_verified_protocols 检查,把本地 SDK 资源直接发往未验证的服务端。 + resource_guard = getattr(self._provider, "_require_provider_resource", None) + if callable(resource_guard): + # 传完整路径(``volcengine.responses.create``)而不是末段方法名: + # ``files.create`` 与 ``responses.create`` 末段相同,只看方法名无法 + # 区分该走哪条能力门禁。 + resource_guard(self._path) # Call results are deliberately not wrapped. They are response models, # pagers, streams, or context managers rather than mutable resource # trees; preserving their SDK identity keeps normal client code intact. diff --git a/src/providers/volcengine.py b/src/providers/volcengine.py index d6bfa08..05702a7 100644 --- a/src/providers/volcengine.py +++ b/src/providers/volcengine.py @@ -56,6 +56,26 @@ def _load_ark_clients() -> tuple[type[Any], type[Any]]: return Ark, AsyncArk + +def _ark_responses_output_text(response: Any) -> str: + """从 Responses 的 ``output[].content[]`` 提取助手正文。 + + Ark 的 ``Response`` 模型没有 ``output_text`` 字段(OpenAI SDK 把它实现为 + 聚合 property),正文只在 ``output`` 项的 ``content`` 里。只取 + ``output_text``/``text`` 类型的内容块,避免把 reasoning 与 refusal 文本 + 混进正文。 + """ + chunks: list[str] = [] + for item in field(response, "output", []) or []: + for content in field(item, "content", []) or []: + if field(content, "type") not in {"output_text", "text"}: + continue + piece = field(content, "text") + if isinstance(piece, str): + chunks.append(piece) + return "".join(chunks) + + class VolcengineProvider(LargeLanguageModel, TextEmbeddingModel): """火山引擎 Ark Provider,支持 Chat Completions、Responses 和 Embeddings。""" @@ -354,6 +374,17 @@ def _normalize_verified_protocols(cls, configured: Any) -> frozenset[str]: normalized.add(protocol) return frozenset(normalized) + def _require_provider_resource(self, method_name: str) -> None: + """在原生资源方法触达 SDK 前校验 Ark 渠道能力。 + + ``method_name`` 可能是显式方法名(``create_response``),也可能是动态 + 资源树路径(``volcengine.responses.create``)。按路径分段判断,避免把 + ``files.create`` 这类同名但无关的调用一并拦下。 + """ + segments = method_name.split(".") + if "responses" in segments or method_name.endswith("_response") or "response" in segments: + self._require_responses_resource(method_name) + def _require_responses_resource(self, operation: str) -> None: """阻止未验证的 Ark Responses 请求到达远端。""" if self._protocol != "responses": @@ -912,12 +943,29 @@ def _build_responses_request(self, request: CompletionRequest) -> dict[str, Any] "Ark Responses", set(params).difference({"extra_body"}), ) - # 官方文档明确:``instructions`` 不可与缓存能力一起使用,``caching`` 配置为 - # ``{"type": "enabled"}`` 时请求会直接报错。SDK 不做本地校验,会原样发到 - # 服务端,因此在构造阶段显式拒绝,避免用户从远端错误反推原因。 - if params.get("instructions") is not None: - enabled_caching = params.get("caching") - if isinstance(enabled_caching, Mapping) and enabled_caching.get("type") == "enabled": + self._reject_instructions_with_enabled_caching(params) + return params + + @staticmethod + def _reject_instructions_with_enabled_caching(params: Mapping[str, Any]) -> None: + """拒绝 ``instructions`` 与 ``caching={"type": "enabled"}`` 同时出现。 + + 官方文档明确二者互斥:配置 ``instructions`` 后本轮请求无法写入或使用 + 缓存,``caching`` 为 ``enabled`` 时服务端直接报错。SDK 不做本地校验, + 会原样发到服务端,因此在构造阶段显式拒绝,避免用户从远端 400 反推。 + + ``caching`` 有两条来源:顶层参数,以及 ``extra_body``(Ark SDK 在 + ``_base_client`` 里把 ``extra_body`` 合并进请求体,服务端看到的仍是 + ``caching=enabled``)。两条都要检查,否则该守卫可被绕过。 + """ + if params.get("instructions") is None: + return + candidates = [params.get("caching")] + extra_body = params.get("extra_body") + if isinstance(extra_body, Mapping): + candidates.append(extra_body.get("caching")) + for candidate in candidates: + if isinstance(candidate, Mapping) and candidate.get("type") == "enabled": raise ValueError( "Ark Responses 的 instructions 与 caching={\"type\": \"enabled\"} 互斥:" "官方规定配置 instructions 后本轮请求无法写入或使用缓存,caching 为 " @@ -927,7 +975,6 @@ def _build_responses_request(self, request: CompletionRequest) -> dict[str, Any] "请显式传入 system_prompt=None 并改用 messages 携带系统提示," "或移除 caching。" ) - return params @classmethod def _validate_native_response_kwargs(cls, kwargs: Mapping[str, Any]) -> dict[str, Any]: @@ -960,6 +1007,10 @@ def _validate_native_response_kwargs(cls, kwargs: Mapping[str, Any]) -> dict[str if not isinstance(params["session"], Mapping): raise ValueError("Ark Responses session 必须是对象。") params["session"] = dict(params["session"]) + # 原生入口同样要执行 instructions 与 caching 的互斥校验,否则用户 + # 绕过 Facade 直接调用 create_response/async_create_response 时会从 + # 远端 400 才得知参数冲突。 + cls._reject_instructions_with_enabled_caching(params) return params def _embedding_options(self) -> dict[str, Any]: @@ -1070,6 +1121,12 @@ def _extract_result(response: Any) -> CompletionResult: output_text = field(response, "output_text") if isinstance(output_text, str): text = output_text + if not text: + # Ark 的 Response 没有 OpenAI 那样的 ``output_text`` 聚合属性, + # 正文位于 ``output[].content[].text``。缺了这一步,所有非流式 + # 入口都会返回空文本而不报错。仅在没有 choices 文本时启用,避免 + # 覆盖 Chat Completions 分支已经取到的内容。 + text = _ark_responses_output_text(response) for item in field(response, "output", []) or []: item_type = field(item, "type") if item_type in {"function_call", "custom_tool_call"}: @@ -1122,6 +1179,22 @@ def _extract_result(response: Any) -> CompletionResult: candidate_refusal = field(content, "refusal") or field(content, "text") if isinstance(candidate_refusal, str) and candidate_refusal: refusal = candidate_refusal + if ( + not choices + and not text + and not calls + and not refusal + and not reasoning + and field(response, "status") == "completed" + ): + # 项目规则禁止用占位结果掩盖失败。Responses 响应标记为 completed + # 却既无正文、无工具调用、也无拒答与推理,说明响应结构与预期不符 + # (例如 SDK 改了字段形状)。此时静默返回空文本会让上层拿到空串后 + # 继续,最终表现为难以定位的「空结果」错误,因此在边界显式失败。 + raise RuntimeError( + "Ark Responses 响应已完成但未包含任何正文、工具调用或拒答内容;" + "请检查响应结构与 SDK 版本是否匹配。" + ) return CompletionResult(text=text, tool_calls=calls, usage=usage_dict, finish_reason=field(choices[0], "finish_reason") if choices else field(response, "status"), response_id=field(response, "id"), refusal=refusal, From c4222a57151733bb0f82a35dd388d8422d187ee3 Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 01:59:12 +0800 Subject: [PATCH 32/35] fix: keep responses-only tool call semantics across normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Ark 专属内容块(input_audio / input_video / image_pixel_limit)写成 content 会被 variant 守卫拒绝,写成顶层 item 却因不带 role/content 而直通请求体, 静默发给不支持它的 OpenAI 端点。补一次等价检查,让两种书写形态一致。 2. Responses custom tool 的 ``input`` 按 SDK 契约是任意文本,但回填时被 统一走 _responses_json_text 强制 json.loads,导致多轮 custom tool 在请求 边界直接失败。改为按类型分流:custom 保留原始文本,function 才做 JSON 规范化。由于 Chat Completions 只接受 ``type: "function"``,归一化时会丢掉 custom 语义,因此新增 keep_responses_type 开关,仅在 Responses 路径保留 原始类型,Chat 路径不受影响(内部字段不会进入请求体)。 3. 流式事件同时给出 ``id``(输出项 ID)与 ``call_id``(调用 ID)两个不同的值, 而 Chat 归一化只保留 ``id``,导致回填 Responses 时 function_call 与 function_call_output 的 call_id 配不上。Responses 路径一并保留 call_id。 Co-Authored-By: Claude Opus 5 (1M context) --- src/providers/__base__/model_provider.py | 103 ++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index c48ccaa..da631a6 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -332,6 +332,7 @@ def _chat_tool_call_item( location: str, *, require_call_id: bool = False, + keep_responses_type: bool = False, ) -> dict[str, Any]: """将工具调用归一化为 OpenAI Chat Completions 的嵌套结构。 @@ -376,9 +377,8 @@ def _chat_tool_call_item( # Chat Completions 的 ``tool_calls[].type`` 只接受 ``function``;Responses 的 # ``function_call``/``custom_tool_call`` 与流式片段都要映射回该取值。 - tool_type = normalized.get("type") - if not isinstance(tool_type, str) or tool_type not in _CHAT_TOOL_CALL_TYPES: - tool_type = "function" + source_type = normalized.get("type") + tool_type = source_type if source_type in _CHAT_TOOL_CALL_TYPES else "function" item: dict[str, Any] = { "type": tool_type, @@ -386,6 +386,20 @@ def _chat_tool_call_item( } if call_id is not None: item["id"] = call_id + # Responses 需要区分 ``custom_tool_call``(其 ``input`` 是任意文本,不做 + # JSON 规范化),但 Chat Completions 只有 ``function`` 一种取值。调用方若 + # 来自 Responses 路径,就在类型被改写时记录原值,避免多轮 custom tool + # 回填被误当 function_call。默认不记录,防止该内部字段进入 Chat 请求体。 + if keep_responses_type and source_type != tool_type: + item["_responses_tool_call_type"] = source_type + # Responses 用 ``call_id`` 关联 ``function_call`` 与 ``function_call_output``, + # 而流式事件会同时给出 ``id``(输出项 ID)与 ``call_id``(调用 ID)两个不同 + # 的值。Chat 归一化只保留 ``id``,会把 ``call_id`` 丢掉,导致回填 Responses + # 时输出项配不上调用。这里在 Responses 路径上一并保留。 + if keep_responses_type: + source_call_id = normalized.get("call_id") + if isinstance(source_call_id, str) and source_call_id.strip(): + item["call_id"] = source_call_id return item @@ -394,8 +408,14 @@ def normalize_chat_messages( location: str = "messages", *, require_tool_call_ids: bool = False, + keep_responses_type: bool = False, ) -> list[dict[str, Any]]: - """归一化 Chat Completions 消息,转换工具调用并保留其它字段。""" + """归一化 Chat Completions 消息,转换工具调用并保留其它字段。 + + ``keep_responses_type`` 由 Responses 路径开启,用于保留 + ``custom_tool_call`` 这类 Chat Completions 不支持的原始调用类型; + Chat 路径保持默认关闭,避免内部字段进入请求体。 + """ normalized: list[dict[str, Any]] = [] for index, message in enumerate(messages): if not isinstance(message, Mapping): @@ -412,6 +432,7 @@ def normalize_chat_messages( tool_call, f"{location}[{index}].tool_calls[{call_index}]", require_call_id=require_tool_call_ids, + keep_responses_type=keep_responses_type, ) for call_index, tool_call in enumerate(tool_calls) ] @@ -425,6 +446,7 @@ def normalize_messages( messages: Sequence[Mapping[str, Any]] | None, *, require_tool_call_ids: bool = False, + keep_responses_type: bool = False, ) -> list[dict[str, Any]]: """生成不修改调用方对象的标准消息列表。 @@ -437,6 +459,7 @@ def normalize_messages( normalized = normalize_chat_messages( messages, require_tool_call_ids=require_tool_call_ids, + keep_responses_type=keep_responses_type, ) if system_prompt and not any(message.get("role") == "system" for message in normalized): normalized.insert(0, {"role": "system", "content": system_prompt}) @@ -487,6 +510,38 @@ def _responses_provider_variant(provider: str) -> str: return "ark" if normalized in {"ark", "volcengine", "volc_engine"} else "openai" +# Ark 专属的 Responses 内容块类型;OpenAI 兼容端点不接受这些块。 +_ARK_ONLY_RESPONSES_ITEM_TYPES = frozenset( + {"input_audio", "audio_url", "input_video", "video_url"} +) + + +def _reject_unsupported_responses_item( + item: Mapping[str, Any], + location: str, + provider: str, +) -> None: + """拒绝经顶层 item 形态绕过 variant 守卫的供应商专属内容块。 + + ``normalize_responses_input`` 对带 ``content`` 的消息走 + ``_responses_message_content``,那里会按 provider 变体拒绝 Ark 专属块。 + 但原生 Responses item(``{"type": "input_audio", ...}``)没有 ``content``, + 会直通到请求体,因此需要在这里做等价检查,避免同一种块因书写位置不同 + 而一个被拒、一个静默发给不支持它的端点。 + """ + if _responses_provider_variant(provider) == "ark": + return + item_type = item.get("type") + if item_type in _ARK_ONLY_RESPONSES_ITEM_TYPES: + raise ValueError( + f"{location} 的 {item_type} 内容块当前不受 Responses SDK 支持。" + ) + if "image_pixel_limit" in item: + raise ValueError( + f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。" + ) + + def _responses_content_part( part: Any, location: str, @@ -793,16 +848,33 @@ def _responses_function_call_item( if not isinstance(name, str) or not name.strip(): raise ValueError(f"{location} 缺少函数名。") - arguments = function_values.get( + raw_arguments = function_values.get( "arguments", tool_call.get("arguments", tool_call.get("input")), ) - item: dict[str, Any] = { - "type": "function_call", - "call_id": call_id, - "name": name, - "arguments": _responses_json_text(arguments, f"{location}.arguments"), - } + # Responses 的 custom tool 按 SDK 契约接受任意文本(``input: str``,无 JSON + # 约束),Provider 自己产出的 ``custom_tool_call`` 也带任意文本。若把它当 + # function_call 强制 json.loads,多轮 custom tool 回填会在请求边界直接失败。 + # 因此按类型分流:custom 保留原始文本,function 才做 JSON 规范化。 + # + # 优先取 _chat_tool_call_item 保留的原始类型:``type`` 已被归一化为 Chat + # Completions 唯一允许的 ``function``,直接读它会丢掉 custom 语义。 + source_type = tool_call.get("_responses_tool_call_type") or tool_call.get("type") + if source_type == "custom_tool_call": + custom_input = "" if raw_arguments is None else str(raw_arguments) + item: dict[str, Any] = { + "type": "custom_tool_call", + "call_id": call_id, + "name": name, + "input": custom_input, + } + else: + item = { + "type": "function_call", + "call_id": call_id, + "name": name, + "arguments": _responses_json_text(raw_arguments, f"{location}.arguments"), + } # A Chat Completions ``id`` is the call identifier, not the Responses # output-item identifier. Do not duplicate it as ``id`` unless the caller # supplied a distinct Responses-style ``call_id`` explicitly. @@ -882,6 +954,9 @@ def normalize_responses_input( system_prompt, messages, require_tool_call_ids=True, + # Responses 需要区分 custom_tool_call 与 function_call:前者的 input + # 是任意文本,不做 JSON 规范化。Chat 路径不开启该开关。 + keep_responses_type=True, ) if messages is None and prompt is not None: return prompt @@ -963,6 +1038,12 @@ def normalize_responses_input( location, provider=provider, ) + else: + # 原生 Responses item 形态(例如 ``{"type": "input_audio", ...}``) + # 不带 role/content,因此不会经过 _responses_message_content 的 + # 内容块转换。同一个 Ark 专属块写成 content 会被拒、写成顶层 item + # 却静默发出,等于绕过了 variant 守卫,因此这里补一次等价检查。 + _reject_unsupported_responses_item(normalized_message, location, provider) converted.append(normalized_message) return converted From ccc37ede5e71166caea03ccebf6ed95d3d8b0ca2 Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 02:00:37 +0800 Subject: [PATCH 33/35] fix: redact credentials in every user-facing error path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chat 路径已有 _safe_exception_text,但 retrieval_test 与顶层 CLI 边界仍把裸 异常交给 console.print 与 logger.error,凭证会原样落到终端与日志。SDK 异常 常回显请求头与 URL,这是最现实的泄漏出口。 把该助手提到 utils/security.py 作为共用实现(safe_exception_text),三处调用 点统一使用,消除「同一场景在一条路径做了脱敏、在另一条没做」的不一致。 测试同步改为从共用位置导入。 Co-Authored-By: Claude Opus 5 (1M context) --- main.py | 3 ++- src/chat/core.py | 29 ++++++++++------------------- src/retrieval_test/core.py | 9 +++++---- tests/chat/test_core.py | 7 ++++--- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index f271f04..efcb21a 100644 --- a/main.py +++ b/main.py @@ -24,6 +24,7 @@ # 从新的配置模块导入 settings 实例 from src.utils.config import get_settings, resolve_app_root +from src.utils.security import safe_exception_text console = Console() VERSION = "1.4.0" # 程序版本 @@ -192,7 +193,7 @@ def main(): console.print("\n[bold yellow]检测到中断信号,正在退出程序... 再见![/bold yellow]") sys.exit(0) except Exception as e: # noqa: BLE001 - top-level CLI boundary reports failures - console.print(f"\n[bold red]程序运行期间发生错误:[/bold red] {e}") + console.print(f"\n[bold red]程序运行期间发生错误:[/bold red] {safe_exception_text(e)}") console.print("[bold red]请检查错误信息并重试。[/bold red]") if __name__ == "__main__": diff --git a/src/chat/core.py b/src/chat/core.py index 001de2d..cf77b25 100644 --- a/src/chat/core.py +++ b/src/chat/core.py @@ -32,16 +32,7 @@ from ..ui.display_utils import display_chat_config, get_relative_path from ..utils.config import get_settings from ..utils.log_manager import get_chat_logger -from ..utils.security import redact_sensitive_text - - -def _safe_exception_text(exc: BaseException) -> str: - """终端显示错误类型和上下文,同时遮蔽常见凭证格式。""" - message = str(exc).strip() - if not message: - return type(exc).__name__ - message = redact_sensitive_text(message) - return f"{type(exc).__name__}: {message[:240]}" +from ..utils.security import safe_exception_text class Chatbot: @@ -101,7 +92,7 @@ def reload_llm(self) -> bool: except Exception as exc: # noqa: BLE001 - reload boundary reports provider failures if new_model is not None and new_model is not self.llm_model: self._dispose_model(new_model) - self.console.print(f"[bold red]重载 LLM 出错: {_safe_exception_text(exc)}[/bold red]") + self.console.print(f"[bold red]重载 LLM 出错: {safe_exception_text(exc)}[/bold red]") return False def _dispose_model(self, model: LargeLanguageModel) -> None: @@ -111,7 +102,7 @@ def _dispose_model(self, model: LargeLanguageModel) -> None: except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures self.logger.warning( "关闭 LLM Provider 失败: %s", - redact_sensitive_text(str(exc)), + safe_exception_text(exc), ) async def _adispose_model(self, model: LargeLanguageModel) -> None: @@ -126,7 +117,7 @@ async def _adispose_model(self, model: LargeLanguageModel) -> None: except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures self.logger.warning( "异步关闭 LLM Provider 失败: %s", - redact_sensitive_text(str(exc)), + safe_exception_text(exc), ) return await asyncio.to_thread(self._dispose_model, model) @@ -146,7 +137,7 @@ def close(self) -> None: except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures self.logger.warning( "关闭检索服务失败: %s", - redact_sensitive_text(str(exc)), + safe_exception_text(exc), ) async def aclose(self) -> None: @@ -166,7 +157,7 @@ async def aclose(self) -> None: except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures self.logger.warning( "异步关闭检索服务失败: %s", - redact_sensitive_text(str(exc)), + safe_exception_text(exc), ) else: close_retrieval = getattr(retrieval_service, "close", None) @@ -178,7 +169,7 @@ async def aclose(self) -> None: except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures self.logger.warning( "关闭检索服务失败: %s", - redact_sensitive_text(str(exc)), + safe_exception_text(exc), ) def apply_config_update(self, updated_config: SessionConfig | dict[str, Any], llm_needs_reload: bool) -> None: @@ -224,7 +215,7 @@ async def apply_config_update_async( await self._adispose_model(new_model) if previous_config is not None: self.session_config = previous_config - self.console.print(f"[bold red]重载 LLM 出错: {_safe_exception_text(exc)}[/bold red]") + self.console.print(f"[bold red]重载 LLM 出错: {safe_exception_text(exc)}[/bold red]") self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]") async def _identify_intent_async(self, user_query: str) -> str: @@ -282,12 +273,12 @@ async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: full_response, ) except Exception as exc: - error_text = redact_sensitive_text(str(exc)) + error_text = safe_exception_text(exc) self.logger.exception( "聊天请求处理失败: %s", error_text, ) - self.console.print(f"[red]LLM 异步生成出错: {_safe_exception_text(exc)}[/red]") + self.console.print(f"[red]LLM 异步生成出错: {safe_exception_text(exc)}[/red]") # 已经输出部分内容时不要追加道歉文本,避免用户收到混合响应; # 具体异常已通过日志和控制台显式记录。 if not full_response: diff --git a/src/retrieval_test/core.py b/src/retrieval_test/core.py index f7c2c11..4a542c9 100644 --- a/src/retrieval_test/core.py +++ b/src/retrieval_test/core.py @@ -16,6 +16,7 @@ from ..ui.display_utils import get_relative_path from ..utils.config import get_settings from ..utils.log_manager import get_module_logger +from ..utils.security import safe_exception_text from .excel_logger import ExcelLogger console = Console() @@ -49,7 +50,7 @@ async def run_retrieval_test_async(): try: excel_logger = ExcelLogger() except Exception as exc: # noqa: BLE001 - interactive setup reports all failures - logger.error("ExcelLogger 初始化失败: %s", exc) + logger.error("ExcelLogger 初始化失败: %s", safe_exception_text(exc)) try: run_config = build_run_config(get_settings()) @@ -57,7 +58,7 @@ async def run_retrieval_test_async(): vector_store = VectorStoreFactory.get_default_vector_store() retrieval_service = RetrievalService(vector_store, EmbeddingService(run_config)) except Exception as exc: # noqa: BLE001 - interactive setup reports all failures - console.print(f"[red]数据库加载失败: {exc}[/red]") + console.print(f"[red]数据库加载失败: {safe_exception_text(exc)}[/red]") return while True: @@ -76,8 +77,8 @@ async def run_retrieval_test_async(): except (KeyboardInterrupt, EOFError): break except Exception as exc: # noqa: BLE001 - interactive loop remains usable - console.print(f"[red]召回测试出错: {exc}[/red]") - logger.error("召回测试出错: %s", exc) + console.print(f"[red]召回测试出错: {safe_exception_text(exc)}[/red]") + logger.error("召回测试出错: %s", safe_exception_text(exc)) def run_retrieval_test(): diff --git a/tests/chat/test_core.py b/tests/chat/test_core.py index 06353af..e16d3b9 100644 --- a/tests/chat/test_core.py +++ b/tests/chat/test_core.py @@ -3,20 +3,21 @@ from rich.console import Console -from src.chat.core import Chatbot, _safe_exception_text, start_chat_session_async +from src.chat.core import Chatbot, start_chat_session_async from src.runtime.contracts import SessionConfig from src.utils.config import ModelDetail, RetrievalMethod from src.utils.log_manager import RedactingFormatter +from src.utils.security import safe_exception_text def test_safe_exception_text_redacts_credentials(): - text = _safe_exception_text(RuntimeError("Authorization: Bearer sk-secret-token")) + text = safe_exception_text(RuntimeError("Authorization: Bearer sk-secret-token")) assert "sk-secret-token" not in text assert "REDACTED" in text def test_safe_exception_text_redacts_json_credentials(): - text = _safe_exception_text(RuntimeError('{"api_key": "secret-value"}')) + text = safe_exception_text(RuntimeError('{"api_key": "secret-value"}')) assert "secret-value" not in text assert "REDACTED" in text From 2f4ebc072cb044902dded51149cbeb539845082b Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 02:03:53 +0800 Subject: [PATCH 34/35] test: cover the credential and provider guards fixed above MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为每条修复补回归测试,并逐条做过红绿验证(先回退修复确认测试失败,再恢复 确认通过),避免写出无论代码对错都会通过的测试: - 脱敏键名遍历 SENSITIVE_OPTION_KEYS,同时断言普通文本(``token usage``、 ``task = value``)不被误伤。 - 掩码凭证在 Bearer 前缀下完整脱敏。 - 畸形 URL 与可解析 URL 同等严格。 - Ark 非流式正文提取改用**真实** SDK 模型构造响应;此前夹具用 ``SimpleNamespace(output_text=...)`` 伪造了 Ark 并不存在的字段,恰好掩盖 了空正文缺陷。 - 动态资源树受 server_verified_protocols 门禁约束,且不误拦 files 等资源。 - instructions × caching 互斥覆盖 extra_body 与原生 kwargs。 - Ark 专属块在顶层 item 形态下同样被拒,Ark 渠道自身仍放行。 - custom_tool_call 往返保留任意文本与空 input,function_call 仍做 JSON 规范化, Chat 路径不泄漏 Responses 专属字段。 - function_call 与 function_call_output 回填后共享同一 call_id。 Co-Authored-By: Claude Opus 5 (1M context) --- tests/providers/test_sdk_capabilities.py | 406 ++++++++++++++++++++ tests/providers/test_security_boundaries.py | 116 ++++++ 2 files changed, 522 insertions(+) diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py index 6870baf..7ba57f9 100644 --- a/tests/providers/test_sdk_capabilities.py +++ b/tests/providers/test_sdk_capabilities.py @@ -5889,3 +5889,409 @@ def test_anthropic_version_parser_requires_a_word_boundary(model_name): provider._options = {} assert provider._sampling_controls_deprecated(model_name) is False + + +# ── 回归:Ark Responses 非流式正文提取 ── + + +def _ark_response(output, *, status="completed"): + """用真实 Ark SDK 模型构造响应,避免伪造线上不存在的字段。 + + 此前测试普遍使用 ``SimpleNamespace(output_text=...)``,但 Ark 的 + ``Response`` 没有 ``output_text``(OpenAI SDK 才把它实现为聚合 property), + 正文位于 ``output[].content[].text``。伪造该字段会让测试永远通过,掩盖 + 真实环境下的空正文缺陷。 + """ + from volcenginesdkarkruntime.types.responses.response import Response + + return Response.model_validate( + { + "id": "resp_test", + "object": "response", + "created_at": 1, + "model": "ark-model", + "status": status, + "tools": [], + "output": output, + } + ) + + +def test_ark_responses_non_stream_extracts_text_from_output_content(): + """Ark 的 Response 没有 ``output_text`` 属性,正文必须从 output 内容块取。 + + 只读 ``output_text`` 会让所有非流式入口返回空文本且不报错,上层 + ``identify_intent`` 随后抛出「意图识别返回空结果」。 + """ + response = _ark_response( + [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [ + {"type": "output_text", "text": "这是真实回答。", "annotations": []} + ], + } + ] + ) + + assert VolcengineProvider._extract_result(response).text == "这是真实回答。" + + +def test_ark_response_model_has_no_output_text_attribute(): + """固定 SDK 事实:Ark 的 Response 不提供 ``output_text``。 + + 该断言失败说明 SDK 改变了响应形状,届时可以重新评估提取逻辑。 + """ + from volcenginesdkarkruntime.types.responses.response import Response + + assert "output_text" not in Response.model_fields + assert not hasattr(Response, "output_text") + + +def test_ark_responses_completed_response_without_any_content_fails_loudly(): + """标记 completed 却无正文、工具调用、拒答与推理时,必须显式失败。 + + 项目规则禁止用占位结果掩盖失败:静默返回空文本会让调用方拿到空串后 + 继续执行,最终表现为难以定位的「空结果」错误。 + """ + with pytest.raises(RuntimeError, match="未包含任何正文"): + VolcengineProvider._extract_result(_ark_response([])) + + +def test_ark_responses_reasoning_only_response_is_still_valid(): + """只有推理内容、没有正文的响应是合法的,不能被 fail-closed 误伤。""" + response = _ark_response( + [ + { + "type": "reasoning", + "id": "rs_1", + "summary": [{"type": "summary_text", "text": "Ark 思考"}], + } + ] + ) + + result = VolcengineProvider._extract_result(response) + + assert result.text == "" + assert result.reasoning == "Ark 思考" + + +# ── 回归:动态资源树必须与显式 Facade 共用能力门禁 ── + + +def _ark_resources_without_verified_protocols(calls): + """构造未登记 server_verified_protocols 的 Ark provider 与资源 Facade。""" + from src.providers.volcengine import ArkResources + + provider = object.__new__(VolcengineProvider) + provider._provider = "volcengine" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + provider._options = {} + object.__setattr__( + provider, + "_client", + SimpleNamespace( + responses=SimpleNamespace( + create=lambda **kwargs: calls.append(kwargs) or "remote", + retrieve=lambda **kwargs: calls.append(kwargs) or "remote", + ), + files=SimpleNamespace(create=lambda **kwargs: "local-file"), + ), + ) + return provider, ArkResources(provider) + + +def test_ark_dynamic_responses_resource_enforces_server_verification(): + """``resources.responses.create(...)`` 必须与 ``create_response()`` 同样受检。 + + 动态资源树此前只做凭证校验,绕过了 server_verified_protocols 门禁,会把 + 本地 SDK 资源直接发往未验证的服务端。 + """ + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + with pytest.raises(ValueError, match="未验证 Responses"): + resources.responses.create(model="ark-model", input="hi") + with pytest.raises(ValueError, match="未验证 Responses"): + resources.responses.retrieve(response_id="resp-1") + + assert calls == [] + + +def test_ark_dynamic_non_responses_resources_are_not_gated(): + """能力门禁只针对 responses 资源,files 等同名资源不能被误拦。""" + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + assert resources.files.create(file="f") == "local-file" + + +def test_ark_dynamic_responses_resource_passes_once_protocol_is_verified(): + """登记 server_verified_protocols 后动态路径应正常放行。""" + calls: list[dict[str, object]] = [] + provider, resources = _ark_resources_without_verified_protocols(calls) + provider._server_verified_protocols = frozenset({"responses"}) + + assert resources.responses.create(model="ark-model", input="hi") == "remote" + assert len(calls) == 1 + + +def test_ark_resource_guard_keeps_credential_check_before_capability_check(): + """凭证问题必须优先报出,不能被能力错误掩盖成配置问题。""" + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + with pytest.raises(ValueError, match="凭证"): + resources.responses.create(model="ark-model", input="hi", api_key="sk-secret") + + +# ── 回归:Ark instructions × caching 互斥必须覆盖所有构造路径 ── + + +def _ark_provider_for_request_build(options=None): + provider = object.__new__(VolcengineProvider) + provider._provider = "volcengine" + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = options or {} + return provider + + +@pytest.mark.parametrize( + "request_kwargs", + [ + pytest.param({"prompt": "hi", "caching": {"type": "enabled"}}, id="top-level-caching"), + pytest.param( + {"prompt": "hi", "extra_body": {"caching": {"type": "enabled"}}}, + id="request-extra-body", + ), + ], +) +def test_ark_responses_rejects_instructions_with_enabled_caching_on_every_path(request_kwargs): + """``caching`` 也可从 ``extra_body`` 进来,Ark SDK 会把它合并进请求体, + 服务端看到的仍是 ``caching=enabled``。只查顶层参数会漏掉这条路径。""" + provider = _ark_provider_for_request_build() + + with pytest.raises(ValueError, match="互斥"): + provider._build_responses_request(CompletionRequest(**request_kwargs)) + + +def test_ark_responses_rejects_caching_enabled_via_model_options_extra_body(): + """模型级 options.extra_body 同样会被并入请求体。""" + provider = _ark_provider_for_request_build( + {"extra_body": {"caching": {"type": "enabled"}}} + ) + + with pytest.raises(ValueError, match="互斥"): + provider._build_responses_request(CompletionRequest(prompt="hi")) + + +def test_ark_native_response_kwargs_reject_instructions_with_enabled_caching(): + """原生 create_response/async_create_response 走 kwargs 校验,也必须受检。""" + with pytest.raises(ValueError, match="互斥"): + VolcengineProvider._validate_native_response_kwargs( + { + "model": "ark-model", + "input": "hi", + "instructions": "system", + "caching": {"type": "enabled"}, + } + ) + + +@pytest.mark.parametrize( + "kwargs", + [ + pytest.param( + {"model": "m", "input": "hi", "caching": {"type": "enabled"}}, id="no-instructions" + ), + pytest.param( + {"model": "m", "input": "hi", "instructions": "s", "caching": {"type": "disabled"}}, + id="caching-disabled", + ), + pytest.param({"model": "m", "input": "hi", "instructions": "s"}, id="no-caching"), + ], +) +def test_ark_native_response_kwargs_allow_non_conflicting_combinations(kwargs): + """合法组合不能被互斥检查误伤。""" + assert VolcengineProvider._validate_native_response_kwargs(kwargs)["model"] == "m" + + +# ── 回归:Ark 专属内容块不能经顶层 item 形态绕过 variant 守卫 ── + + +@pytest.mark.parametrize( + "item", + [ + {"type": "input_audio", "audio": "AAAA"}, + {"type": "input_video", "video": "BBBB"}, + {"type": "input_image", "image_url": "u", "image_pixel_limit": {"max_pixels": 100}}, + ], +) +def test_openai_responses_rejects_ark_only_blocks_as_top_level_items(item): + """同一个 Ark 专属块写成 content 会被拒、写成顶层 item 却直通请求体, + 等于绕过了 variant 守卫。两种形态必须一致拒绝。""" + with pytest.raises(ValueError): + normalize_responses_input(None, None, [dict(item)], provider="openai") + + +def test_ark_provider_allows_ark_only_blocks_as_top_level_items(): + """Ark 渠道本身要能发这些块,守卫不能把合法用法一并拦下。""" + converted = normalize_responses_input( + None, + None, + [{"type": "input_audio", "audio_url": "https://example.invalid/a.mp3"}], + provider="ark", + ) + + assert converted[0]["type"] == "input_audio" + + +# ── 回归:custom_tool_call 回填不能被强制 JSON 解析 ── + + +def test_responses_custom_tool_call_round_trip_preserves_free_form_input(): + """Responses custom tool 的 ``input`` 是任意文本(SDK 契约无 JSON 约束), + 强制 json.loads 会让多轮 custom tool 在请求边界直接失败。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "type": "custom_tool_call", + "id": "ctc_1", + "call_id": "call_abc", + "name": "my_custom_tool", + "arguments": "any free-form text, not JSON", + } + ], + } + ], + provider="openai", + ) + + assert converted[0]["type"] == "custom_tool_call" + assert converted[0]["input"] == "any free-form text, not JSON" + assert converted[0]["call_id"] == "call_abc" + + +def test_responses_custom_tool_call_accepts_empty_input(): + """空 input 是合法值(custom tool 无参数),不能被当成无效 JSON 拒绝。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + {"type": "custom_tool_call", "call_id": "c", "name": "f", "arguments": ""} + ], + } + ], + provider="openai", + ) + + assert converted[0]["input"] == "" + + +def test_responses_function_call_still_normalizes_json_arguments(): + """function_call 必须继续做 JSON 规范化,不能被 custom 分支影响。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + {"type": "function_call", "call_id": "c1", "name": "f", "arguments": '{"a":1}'} + ], + } + ], + provider="openai", + ) + + assert converted[0]["type"] == "function_call" + assert converted[0]["arguments"] == '{"a":1}' + + +def test_chat_messages_do_not_leak_responses_only_type_marker(): + """Chat Completions 路径不能带上 Responses 专属的内部字段,否则它会随 + 请求体发给 API。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "c1", "type": "custom_tool_call", "name": "n", "input": '{"a":1}'} + ], + } + ] + ) + + assert normalized[0]["tool_calls"] == [ + {"id": "c1", "type": "function", "function": {"name": "n", "arguments": '{"a":1}'}} + ] + + +def test_responses_round_trip_keeps_call_id_for_streamed_tool_calls(): + """流式事件同时给出 ``id``(输出项 ID)与 ``call_id``(调用 ID),两者不同。 + + Responses 用 ``call_id`` 关联 ``function_call`` 与 ``function_call_output``, + 若 Chat 归一化只保留 ``id``,回填时输出项就配不上调用。 + """ + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "fc_0123", + "call_id": "call_abc", + "type": "function_call", + "name": "f", + "arguments": "{}", + } + ], + } + ], + provider="openai", + ) + + assert converted[0]["call_id"] == "call_abc" + assert converted[0]["id"] == "fc_0123" + + +def test_responses_function_call_output_pairs_with_preserved_call_id(): + """回填的 function_call 与 function_call_output 必须共享同一 call_id。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "fc_0123", + "call_id": "call_abc", + "type": "function_call", + "name": "f", + "arguments": "{}", + } + ], + }, + {"role": "tool", "tool_call_id": "call_abc", "content": "result"}, + ], + provider="openai", + ) + + assert converted[0]["call_id"] == converted[1]["call_id"] == "call_abc" diff --git a/tests/providers/test_security_boundaries.py b/tests/providers/test_security_boundaries.py index ef81b8c..9343d80 100644 --- a/tests/providers/test_security_boundaries.py +++ b/tests/providers/test_security_boundaries.py @@ -71,3 +71,119 @@ def test_security_redacts_masked_credential_shapes(value): assert "sk-abc" not in redacted assert "rk-abc" not in redacted assert "sess-abcd" not in redacted + + +# ── 回归:脱敏与配置边界必须使用同一套凭证键名 ── + +# 这些键在 is_sensitive_option_key 中返回 True,却曾因文本正则词表更小而在 +# 日志里明文输出(client_secret 是 Google OAuth 的常见形态)。 +_TEXT_REDACTION_KEYS = [ + "client_secret", + "clientsecret", + "private_key", + "privatekey", + "credentials", + "ssh_private_key", + "secret_access_key", + "encryption_key", + "webhook_secret", +] + + +@pytest.mark.parametrize("key", _TEXT_REDACTION_KEYS) +def test_security_redacts_every_sensitive_key_name_in_text(key): + """配置边界判为敏感的键名,在文本里也必须脱敏。""" + secret = "SUPERSECRETVALUE12345" + assert is_sensitive_option_key(key) is True + assert secret not in redact_sensitive_text(f"{key}={secret}") + + +@pytest.mark.parametrize("key", _TEXT_REDACTION_KEYS) +def test_security_redacts_sensitive_key_names_in_url_query(key): + """URL query 形态同样要覆盖,不能只处理 key=value。""" + secret = "SUPERSECRETVALUE12345" + assert secret not in redact_sensitive_text(f"https://example.invalid/x?{key}={secret}") + + +@pytest.mark.parametrize( + "text", + ["token usage is high", "task = value", "the secret sauce", "auth flow"], +) +def test_security_keeps_ordinary_text_intact(text): + """扩大键名词表后,普通文本不能被误脱敏。""" + assert redact_sensitive_text(text) == text + + +# ── 回归:掩码形态在 Bearer 前缀下也要完整脱敏 ── + + +@pytest.mark.parametrize( + "value", + [ + "Bearer sk-abc***...***xyz rejected", + "Authorization: Bearer rk-abcd****wxyz", + "Bearer sess-abcdef****tail", + "sk-abc***...***xyz", + "rk-abcd****wxyz", + ], +) +def test_security_redacts_masked_credentials_after_bearer_prefix(value): + """``_BEARER_TEXT_RE`` 会先吃掉 ``Bearer sk-abc`` 前缀,使掩码正则失配, + 导致尾部掩码片段残留。掩码规则必须优先于 Bearer 规则执行。""" + redacted = redact_sensitive_text(value) + assert "***" not in redacted + assert "****" not in redacted + + +# ── 回归:urlsplit 无法解析的值不能让凭证整体绕过边界 ── + + +def test_security_rejects_credential_hidden_behind_unparseable_url(): + """``urlsplit`` 对畸形 URL 抛 ValueError。若该分支直接 return,任何凭证 + 都能随一个畸形前缀整体绕过边界校验(这是叶子值的唯一检查入口)。""" + unparseable = "https://[::1 token sk-abcdefghijklmnopqrstuvwxyz0123456789" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": unparseable}, "Provider") + + +def test_security_parseable_and_unparseable_urls_are_equally_strict(): + """同样藏凭证的两个值,只因 URL 可解析与否而一个被拒一个放行,即为漏洞。""" + secret = "token sk-abcdefghijklmnopqrstuvwxyz0123456789" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": f"https://h/x {secret}"}, "Provider") + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": f"https://[::1 {secret}"}, "Provider") + + +# ── 回归:面向用户的异常输出必须脱敏 ── + + +def test_safe_exception_text_redacts_credentials(): + """SDK 异常常回显请求头或 URL,终端与日志是凭证最容易泄漏的出口。""" + from src.utils.security import safe_exception_text + + secret = "sk-abcdefghijklmnopqrstuvwxyz0123456789" + rendered = safe_exception_text(RuntimeError(f"auth failed for {secret}")) + + assert secret not in rendered + assert "RuntimeError" in rendered + + +def test_safe_exception_text_handles_empty_and_long_messages(): + from src.utils.security import safe_exception_text + + assert safe_exception_text(ValueError()) == "ValueError" + assert len(safe_exception_text(RuntimeError("x" * 500))) <= 260 + + +def test_user_facing_error_paths_do_not_print_raw_exceptions(): + """``retrieval_test`` 与顶层入口曾把裸异常交给 console/logger,凭证会 + 原样落到终端与日志;``chat`` 路径已做脱敏,三者必须一致。""" + import pathlib + + root = pathlib.Path(__file__).resolve().parents[2] + for relative in ("src/retrieval_test/core.py", "main.py", "src/chat/core.py"): + source = (root / relative).read_text(encoding="utf-8") + assert "safe_exception_text" in source, relative + assert 'console.print(f"[red]召回测试出错: {exc}' not in source, relative + assert '{e}")' not in source, relative From 34b8319700a40f27148da7ef6d02f9fa1d0ee2e0 Mon Sep 17 00:00:00 2001 From: Mison Date: Tue, 22 Sep 2026 08:54:40 +0800 Subject: [PATCH 35/35] fix: include the Apache 2.0 text and release teardown gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对抗式审查的完整性复核指出三项遗留问题,逐条核实后处理: 1. 仓库内没有 Apache 2.0 全文。Dify 衍生代码(src/etl/、src/retrieval/ 等) 遵循修改后的 Apache 2.0,而 DIFY_LICENSE 只是「引用 Apache 2.0 + 附加 条款」的摘要。该许可证第 4(a) 条要求随分发提供许可证副本,此前移除 Dify 子模块时把唯一的副本一并删掉了。补入 licenses/APACHE-2.0.txt, 纳入发布包 PACKAGE_FILES,并在 DIFY_LICENSE 中指向它。 PACKAGE_FILES 现在含子目录路径,copy2 不会创建父目录,一并补上。 2. retrieval_test 创建 RetrievalService/EmbeddingService 后从不释放, 而 chat 与 retriever 路径都做了。Embedding provider 与向量存储持有 SDK 客户端与连接池,退出时不释放会留下未关闭资源。用 try/finally 包裹 交互循环,释放失败只记录警告,不掩盖主流程结果。 3. PyRAG-Kit.spec 是构建产物(生成后即 unlink)却未进 .gitignore,工作区 容易误提交。补 *.spec。 以下两项经核实**不成立**,未改动: - 称 requirements.txt 由「仅生产依赖」变为「含 dev 工具链」属未申报变更。 实测 main 版本的 requirements.txt 同样含 pyinstaller 与 pytest——它是 uv export 的默认全量导出,口径未变,本次只是 dev 分组新增了三个工具。 - 称 recursive_text_splitter.py 与 snapshot_repository.py 属移植代码却缺 Dify 归属头。两者分别完全建立在 LangChain 的 RecursiveCharacterTextSplitter 与本项目的 src.runtime.contracts 之上,不含 Dify 特有逻辑;补一个不实的 归属头反而是错误。 Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + DIFY_LICENSE | 6 +- licenses/APACHE-2.0.txt | 202 ++++++++++++++++++++++++++++++++ scripts/build_binary_release.py | 9 +- src/retrieval_test/core.py | 56 ++++++--- 5 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 licenses/APACHE-2.0.txt diff --git a/.gitignore b/.gitignore index d101b87..f56791b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ htmlcov/ *wheels/ *.egg *instance/ +# PyInstaller 构建产物(scripts/build_binary_release.py 生成后即删除,不应入库) +*.spec # 虚拟环境 /.env* !/.env.example diff --git a/DIFY_LICENSE b/DIFY_LICENSE index a209de7..fb74e97 100644 --- a/DIFY_LICENSE +++ b/DIFY_LICENSE @@ -15,7 +15,11 @@ b. LOGO and copyright information: In the process of using Dify's frontend, you a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary. b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations. -Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0. +Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. + +A copy of the Apache License 2.0 is included in this repository at `licenses/APACHE-2.0.txt` +and is redistributed with every release bundle, as required by section 4(a) of that license. +The canonical text is also available at http://www.apache.org/licenses/LICENSE-2.0. The interactive design of this product is protected by appearance patent. diff --git a/licenses/APACHE-2.0.txt b/licenses/APACHE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/licenses/APACHE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/build_binary_release.py b/scripts/build_binary_release.py index 2979d32..e07246d 100755 --- a/scripts/build_binary_release.py +++ b/scripts/build_binary_release.py @@ -22,6 +22,9 @@ "README.md", "LICENSE", "DIFY_LICENSE", + # Dify 衍生代码遵循修改后的 Apache 2.0,该许可证第 4(a) 条要求随分发 + # 提供许可证副本;DIFY_LICENSE 只是引用它的摘要,不能替代全文。 + "licenses/APACHE-2.0.txt", "config.toml.example", ".env.example", ] @@ -164,7 +167,11 @@ def stage_bundle(target: str, version: str) -> Path: shutil.copytree(app_source, app_target) for relative_file in PACKAGE_FILES: - shutil.copy2(PROJECT_ROOT / relative_file, bundle_root / relative_file) + target = bundle_root / relative_file + # PACKAGE_FILES 含子目录路径(如 licenses/APACHE-2.0.txt), + # copy2 不会自动创建父目录。 + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(PROJECT_ROOT / relative_file, target) prepare_runtime_layout(bundle_root) return bundle_root diff --git a/src/retrieval_test/core.py b/src/retrieval_test/core.py index 4a542c9..961e1de 100644 --- a/src/retrieval_test/core.py +++ b/src/retrieval_test/core.py @@ -1,5 +1,7 @@ import asyncio +import inspect import os +from typing import Any from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -42,6 +44,20 @@ def display_results(query: str, documents: list[dict]): console.print(table) + +async def _aclose_quietly(service: Any) -> None: + """释放服务持有的资源,失败只记录警告,不打断退出流程。""" + close = getattr(service, "aclose", None) or getattr(service, "close", None) + if not callable(close): + return + try: + result = close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown must not mask the main flow + logger.warning("释放检索服务失败: %s", safe_exception_text(exc)) + + async def run_retrieval_test_async(): console.print(Panel("进入异步召回测试模式。输入 '/quit' 退出。", border_style="yellow")) session = PromptSession() @@ -61,24 +77,30 @@ async def run_retrieval_test_async(): console.print(f"[red]数据库加载失败: {safe_exception_text(exc)}[/red]") return - while True: - try: - query = await session.prompt_async(HTML('测试查询: ')) - if query.lower() == "/quit": + try: + while True: + try: + query = await session.prompt_async(HTML('测试查询: ')) + if query.lower() == "/quit": + break + if not query: + continue + + with console.status("[bold green]正在异步检索...[/bold green]"): + documents = await retrieval_service.retrieve(query, session_config, console=console) + display_results(query, documents) + if excel_logger: + excel_logger.log_results(query, documents) + except (KeyboardInterrupt, EOFError): break - if not query: - continue - - with console.status("[bold green]正在异步检索...[/bold green]"): - documents = await retrieval_service.retrieve(query, session_config, console=console) - display_results(query, documents) - if excel_logger: - excel_logger.log_results(query, documents) - except (KeyboardInterrupt, EOFError): - break - except Exception as exc: # noqa: BLE001 - interactive loop remains usable - console.print(f"[red]召回测试出错: {safe_exception_text(exc)}[/red]") - logger.error("召回测试出错: %s", safe_exception_text(exc)) + except Exception as exc: # noqa: BLE001 - interactive loop remains usable + console.print(f"[red]召回测试出错: {safe_exception_text(exc)}[/red]") + logger.error("召回测试出错: %s", safe_exception_text(exc)) + finally: + # 与 chat 路径保持一致:Embedding provider 与向量存储持有 SDK 客户端和 + # 连接池,退出时不释放会留下未关闭的资源。释放失败不掩盖主流程结果, + # 只记录警告。 + await _aclose_quietly(retrieval_service) def run_retrieval_test():