From 0de1d99da9eeabb55b9203369bd43779961118da Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 3 Dec 2025 10:33:06 -0800 Subject: [PATCH 01/24] Upgrade to FastMCP 2.13.0 --- .../amazon_keyspaces_mcp_server/server.py | 2 +- .../pyproject.toml | 2 +- src/amazon-keyspaces-mcp-server/uv.lock | 1189 +++++++++++------ 3 files changed, 748 insertions(+), 445 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 28ad191fa5..1d1a9222dc 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -32,7 +32,7 @@ ) from .services import DataService, QueryAnalysisService, SchemaService from loguru import logger -from mcp.server.fastmcp import Context, FastMCP +from fastmcp import Context, FastMCP from pydantic import Field from typing import Any, Optional diff --git a/src/amazon-keyspaces-mcp-server/pyproject.toml b/src/amazon-keyspaces-mcp-server/pyproject.toml index 81608ffe7b..5d61e09b94 100644 --- a/src/amazon-keyspaces-mcp-server/pyproject.toml +++ b/src/amazon-keyspaces-mcp-server/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10,<3.12" dependencies = [ "boto3>=1.37.27", "cassandra-driver>=3.25.0", - "fastmcp>=0.1.0", + "fastmcp>=2.8.0", "loguru>=0.7.0", "mcp>=1.11.0", "pydantic>=2.10.6", diff --git a/src/amazon-keyspaces-mcp-server/uv.lock b/src/amazon-keyspaces-mcp-server/uv.lock index 313ba01808..1a3a5bd820 100644 --- a/src/amazon-keyspaces-mcp-server/uv.lock +++ b/src/amazon-keyspaces-mcp-server/uv.lock @@ -1,14 +1,14 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.10, <3.12" [[package]] name = "annotated-types" version = "0.7.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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { 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" }, ] [[package]] @@ -21,32 +21,44 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[package]] name = "argcomplete" version = "3.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403 } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708 }, + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, ] [[package]] name = "awslabs-amazon-keyspaces-mcp-server" -version = "0.0.2" +version = "0.0.6" source = { editable = "." } dependencies = [ { name = "boto3" }, @@ -74,7 +86,7 @@ dev = [ requires-dist = [ { name = "boto3", specifier = ">=1.37.27" }, { name = "cassandra-driver", specifier = ">=3.25.0" }, - { name = "fastmcp", specifier = ">=0.1.0" }, + { name = "fastmcp", specifier = ">=2.8.0" }, { name = "loguru", specifier = ">=0.7.0" }, { name = "mcp", specifier = ">=1.11.0" }, { name = "pydantic", specifier = ">=2.10.6" }, @@ -93,6 +105,15 @@ dev = [ { name = "ruff", specifier = ">=0.9.7" }, ] +[[package]] +name = "beartype" +version = "0.22.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/1d/794ae2acaa67c8b216d91d5919da2606c2bb14086849ffde7f5555f3a3a5/beartype-0.22.8.tar.gz", hash = "sha256:b19b21c9359722ee3f7cc433f063b3e13997b27ae8226551ea5062e621f61165", size = 1602262, upload-time = "2025-12-03T05:11:10.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2a/fbcbf5a025d3e71ddafad7efd43e34ec4362f4d523c3c471b457148fb211/beartype-0.22.8-py3-none-any.whl", hash = "sha256:b832882d04e41a4097bab9f63e6992bc6de58c414ee84cba9b45b67314f5ab2e", size = 1331895, upload-time = "2025-12-03T05:11:08.373Z" }, +] + [[package]] name = "boto3" version = "1.38.17" @@ -102,9 +123,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/dd/68ea8ab6dfbed46b75fcfe0bbd5ae19e4d3ef094b749ff8d944398e90f2d/boto3-1.38.17.tar.gz", hash = "sha256:6058feef976ece2878ad3555f39933e63d20d02e2bbd40610ab2926d4555710a", size = 111803 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/dd/68ea8ab6dfbed46b75fcfe0bbd5ae19e4d3ef094b749ff8d944398e90f2d/boto3-1.38.17.tar.gz", hash = "sha256:6058feef976ece2878ad3555f39933e63d20d02e2bbd40610ab2926d4555710a", size = 111803, upload-time = "2025-05-15T19:35:17.029Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/89/634155fb209f50fd98da1cb11480bcdf6ed8d8ab68800d91cdb2bf59a8af/boto3-1.38.17-py3-none-any.whl", hash = "sha256:9b56c98fe7acb6559c24dacd838989878c60f3df2fb8ca5f311128419fd9f953", size = 139937 }, + { url = "https://files.pythonhosted.org/packages/ce/89/634155fb209f50fd98da1cb11480bcdf6ed8d8ab68800d91cdb2bf59a8af/boto3-1.38.17-py3-none-any.whl", hash = "sha256:9b56c98fe7acb6559c24dacd838989878c60f3df2fb8ca5f311128419fd9f953", size = 139937, upload-time = "2025-05-15T19:35:14.663Z" }, ] [[package]] @@ -116,9 +137,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/73/8b831403be00dbea152d4827929a5772f58e0413dd3e6b6d4b3592d88d39/botocore-1.38.17.tar.gz", hash = "sha256:f2db4c4bdcfbc41d78bfe73b9affe7d217c7840f8ce120cff815536969418b18", size = 13903448 } +sdist = { url = "https://files.pythonhosted.org/packages/33/73/8b831403be00dbea152d4827929a5772f58e0413dd3e6b6d4b3592d88d39/botocore-1.38.17.tar.gz", hash = "sha256:f2db4c4bdcfbc41d78bfe73b9affe7d217c7840f8ce120cff815536969418b18", size = 13903448, upload-time = "2025-05-15T19:35:05.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/fc/9c08db2e89055999e996fee3537cbbfb4ed8ebf0d4ab1b1045e1819b76d8/botocore-1.38.17-py3-none-any.whl", hash = "sha256:ec75cf02fbd3dbec18187085ce387761eab16afdccfd0774fd168db3689c6cb6", size = 13564514, upload-time = "2025-05-15T19:35:00.231Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/fc/9c08db2e89055999e996fee3537cbbfb4ed8ebf0d4ab1b1045e1819b76d8/botocore-1.38.17-py3-none-any.whl", hash = "sha256:ec75cf02fbd3dbec18187085ce387761eab16afdccfd0774fd168db3689c6cb6", size = 13564514 }, + { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, ] [[package]] @@ -128,73 +158,109 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "geomet" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/6f/d25121afaa2ea0741d05d2e9921a7ca9b4ce71634b16a8aaee21bd7af818/cassandra-driver-3.29.2.tar.gz", hash = "sha256:c4310a7d0457f51a63fb019d8ef501588c491141362b53097fbc62fa06559b7c", size = 293752 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/6f/d25121afaa2ea0741d05d2e9921a7ca9b4ce71634b16a8aaee21bd7af818/cassandra-driver-3.29.2.tar.gz", hash = "sha256:c4310a7d0457f51a63fb019d8ef501588c491141362b53097fbc62fa06559b7c", size = 293752, upload-time = "2024-09-10T02:20:46.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/b4/d5da6b2e82abc8b1d9f93bbc633441a51098bb183aaf2c0481162e17fffe/cassandra_driver-3.29.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:957208093ff2353230d0d83edf8c8e8582e4f2999d9a33292be6558fec943562", size = 363775 }, - { url = "https://files.pythonhosted.org/packages/f4/6d/366346a652f8523c26307846ec5c59e93fdfeee28e67078d68a07fcb2da2/cassandra_driver-3.29.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d70353b6d9d6e01e2b261efccfe90ce0aa6f416588e6e626ca2ed0aff6b540cf", size = 364096 }, - { url = "https://files.pythonhosted.org/packages/cc/60/f8de88175937481be98da88eb88b4fd704093e284e5907775293c496df32/cassandra_driver-3.29.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ad489e4df2cc7f41d3aca8bd8ddeb8071c4fb98240ed07f1dcd9b5180fd879", size = 3660567 }, - { url = "https://files.pythonhosted.org/packages/3b/3a/354db5ac8349ba5dd9827f43c2436221387368f48db50b030ded8cdf91ea/cassandra_driver-3.29.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f1dfa33c3d93350057d6dc163bb92748b6e6a164c408c75cf2c59be0a203b7", size = 3948499 }, - { url = "https://files.pythonhosted.org/packages/a5/bd/7c62675d722f99097934675468636fdabd42b1e418e9fc567562ee2142d7/cassandra_driver-3.29.2-cp310-cp310-win32.whl", hash = "sha256:f9df1e6ae4201eb2eae899cb0649d46b3eb0843f075199b51360bc9d59679a31", size = 340917 }, - { url = "https://files.pythonhosted.org/packages/18/fa/9c73f0d416167097db871dd46e09a327a138b264774e3dbed5159a8ebdd2/cassandra_driver-3.29.2-cp310-cp310-win_amd64.whl", hash = "sha256:c4a005bc0b4fd8b5716ad931e1cc788dbd45967b0bcbdc3dfde33c7f9fde40d4", size = 348622 }, - { url = "https://files.pythonhosted.org/packages/d8/aa/d332d2e10585772e9a4703d524fc818613e7301527a1534f22022b02e9ab/cassandra_driver-3.29.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e31cee01a6fc8cf7f32e443fa0031bdc75eed46126831b7a807ab167b4dc1316", size = 363772 }, - { url = "https://files.pythonhosted.org/packages/f8/26/adc5beac60c373733569868ee1843691fae5d9d8cd07a4907e7c4a55bdaa/cassandra_driver-3.29.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52edc6d4bd7d07b10dc08b7f044dbc2ebe24ad7009c23a65e0916faed1a34065", size = 364100 }, - { url = "https://files.pythonhosted.org/packages/dc/9b/af6cc4ba2cd56773e9f47ee93c2afca374c4a6ee62eaf6890ae65176cd16/cassandra_driver-3.29.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb3a9f24fc84324d426a69dc35df66de550833072a4d9a4d63d72fda8fcaecb9", size = 3643143 }, - { url = "https://files.pythonhosted.org/packages/fd/03/85a1bcfb463896c5391b9b3315f7d9536b0402afdcab78c793911765c99b/cassandra_driver-3.29.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e89de04809d02bb1d5d03c0946a7baaaf85e93d7e6414885b4ea2616efe9de0", size = 3920657 }, - { url = "https://files.pythonhosted.org/packages/17/3f/480af48ce578970b97878990ac3a44d07e185ddb04057660f54f393fea05/cassandra_driver-3.29.2-cp311-cp311-win32.whl", hash = "sha256:7104e5043e9cc98136d7fafe2418cbc448dacb4e1866fe38ff5be76f227437ef", size = 340920 }, - { url = "https://files.pythonhosted.org/packages/86/57/63654b85a2e4fa3af6afa8e883fdad658cba9d7565d098ac281a358abf8c/cassandra_driver-3.29.2-cp311-cp311-win_amd64.whl", hash = "sha256:69aa53f1bdb23487765faa92eef57366637878eafc412f46af999e722353b22f", size = 348625 }, + { url = "https://files.pythonhosted.org/packages/54/b4/d5da6b2e82abc8b1d9f93bbc633441a51098bb183aaf2c0481162e17fffe/cassandra_driver-3.29.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:957208093ff2353230d0d83edf8c8e8582e4f2999d9a33292be6558fec943562", size = 363775, upload-time = "2024-09-10T02:19:21.978Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6d/366346a652f8523c26307846ec5c59e93fdfeee28e67078d68a07fcb2da2/cassandra_driver-3.29.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d70353b6d9d6e01e2b261efccfe90ce0aa6f416588e6e626ca2ed0aff6b540cf", size = 364096, upload-time = "2024-09-10T02:19:24.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/60/f8de88175937481be98da88eb88b4fd704093e284e5907775293c496df32/cassandra_driver-3.29.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ad489e4df2cc7f41d3aca8bd8ddeb8071c4fb98240ed07f1dcd9b5180fd879", size = 3660567, upload-time = "2024-09-10T02:19:27.874Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/354db5ac8349ba5dd9827f43c2436221387368f48db50b030ded8cdf91ea/cassandra_driver-3.29.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f1dfa33c3d93350057d6dc163bb92748b6e6a164c408c75cf2c59be0a203b7", size = 3948499, upload-time = "2024-09-10T02:19:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bd/7c62675d722f99097934675468636fdabd42b1e418e9fc567562ee2142d7/cassandra_driver-3.29.2-cp310-cp310-win32.whl", hash = "sha256:f9df1e6ae4201eb2eae899cb0649d46b3eb0843f075199b51360bc9d59679a31", size = 340917, upload-time = "2024-09-10T02:19:37.652Z" }, + { url = "https://files.pythonhosted.org/packages/18/fa/9c73f0d416167097db871dd46e09a327a138b264774e3dbed5159a8ebdd2/cassandra_driver-3.29.2-cp310-cp310-win_amd64.whl", hash = "sha256:c4a005bc0b4fd8b5716ad931e1cc788dbd45967b0bcbdc3dfde33c7f9fde40d4", size = 348622, upload-time = "2024-09-10T02:19:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/d8/aa/d332d2e10585772e9a4703d524fc818613e7301527a1534f22022b02e9ab/cassandra_driver-3.29.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e31cee01a6fc8cf7f32e443fa0031bdc75eed46126831b7a807ab167b4dc1316", size = 363772, upload-time = "2024-09-10T02:19:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/f8/26/adc5beac60c373733569868ee1843691fae5d9d8cd07a4907e7c4a55bdaa/cassandra_driver-3.29.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52edc6d4bd7d07b10dc08b7f044dbc2ebe24ad7009c23a65e0916faed1a34065", size = 364100, upload-time = "2024-09-10T02:19:43.412Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/af6cc4ba2cd56773e9f47ee93c2afca374c4a6ee62eaf6890ae65176cd16/cassandra_driver-3.29.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb3a9f24fc84324d426a69dc35df66de550833072a4d9a4d63d72fda8fcaecb9", size = 3643143, upload-time = "2024-09-10T02:19:47.932Z" }, + { url = "https://files.pythonhosted.org/packages/fd/03/85a1bcfb463896c5391b9b3315f7d9536b0402afdcab78c793911765c99b/cassandra_driver-3.29.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e89de04809d02bb1d5d03c0946a7baaaf85e93d7e6414885b4ea2616efe9de0", size = 3920657, upload-time = "2024-09-10T02:19:52.524Z" }, + { url = "https://files.pythonhosted.org/packages/17/3f/480af48ce578970b97878990ac3a44d07e185ddb04057660f54f393fea05/cassandra_driver-3.29.2-cp311-cp311-win32.whl", hash = "sha256:7104e5043e9cc98136d7fafe2418cbc448dacb4e1866fe38ff5be76f227437ef", size = 340920, upload-time = "2024-09-10T02:19:54.623Z" }, + { url = "https://files.pythonhosted.org/packages/86/57/63654b85a2e4fa3af6afa8e883fdad658cba9d7565d098ac281a358abf8c/cassandra_driver-3.29.2-cp311-cp311-win_amd64.whl", hash = "sha256:69aa53f1bdb23487765faa92eef57366637878eafc412f46af999e722353b22f", size = 348625, upload-time = "2024-09-10T02:19:56.164Z" }, ] [[package]] name = "certifi" version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { 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" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] @@ -204,18 +270,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -235,39 +301,39 @@ dependencies = [ { name = "tomlkit" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/dc/a993a0744fbdb81e08536f3f7f85cd479da54e5a74589e0f287b8add77a9/commitizen-4.7.1.tar.gz", hash = "sha256:7eba472ef582b1bf2480164901ed542173e83f4f885968e7bed0b8cd0f6dfb07", size = 53290 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/dc/a993a0744fbdb81e08536f3f7f85cd479da54e5a74589e0f287b8add77a9/commitizen-4.7.1.tar.gz", hash = "sha256:7eba472ef582b1bf2480164901ed542173e83f4f885968e7bed0b8cd0f6dfb07", size = 53290, upload-time = "2025-05-16T02:50:59.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/1e/30d492dc7c17727d0b08cffb40dab5a6fe86767cb9d96eeafe06d4163c26/commitizen-4.7.1-py3-none-any.whl", hash = "sha256:80b08328b9741483d2de01cd76d015e9f6dba62c0ae450d4f8606ee3cbf9aa0e", size = 76035 }, + { url = "https://files.pythonhosted.org/packages/64/1e/30d492dc7c17727d0b08cffb40dab5a6fe86767cb9d96eeafe06d4163c26/commitizen-4.7.1-py3-none-any.whl", hash = "sha256:80b08328b9741483d2de01cd76d015e9f6dba62c0ae450d4f8606ee3cbf9aa0e", size = 76035, upload-time = "2025-05-16T02:50:57.904Z" }, ] [[package]] name = "coverage" version = "7.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, - { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, - { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, - { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, - { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, - { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, - { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, - { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, - { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, - { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, - { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, - { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, - { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, - { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, - { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, - { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, - { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, - { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, - { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, - { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, - { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload-time = "2025-03-30T20:34:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload-time = "2025-03-30T20:34:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload-time = "2025-03-30T20:34:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload-time = "2025-03-30T20:35:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload-time = "2025-03-30T20:35:02.307Z" }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload-time = "2025-03-30T20:35:04.141Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload-time = "2025-03-30T20:35:05.889Z" }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload-time = "2025-03-30T20:35:07.76Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload-time = "2025-03-30T20:35:09.144Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload-time = "2025-03-30T20:35:10.734Z" }, + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, ] [package.optional-dependencies] @@ -275,22 +341,138 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/fe026df2ab8301e30a2b0bd425ff1462ad858fd4f991c1ac0389c2059c24/cyclopts-4.3.0.tar.gz", hash = "sha256:e95179cd0a959ce250ecfb2f0262a5996a92c1f9467bccad2f3d829e6833cef5", size = 151411, upload-time = "2025-11-25T02:59:33.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/e8/77a231ae531cf38765b75ddf27dae28bb5f70b41d8bb4f15ce1650e93f57/cyclopts-4.3.0-py3-none-any.whl", hash = "sha256:91a30b69faf128ada7cfeaefd7d9649dc222e8b2a8697f1fc99e4ee7b7ca44f3", size = 187184, upload-time = "2025-11-25T02:59:32.21Z" }, +] + [[package]] name = "decli" version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424, upload-time = "2024-04-28T17:41:05.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854, upload-time = "2024-04-28T17:41:04.663Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854 }, + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, + { 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" }, +] + +[[package]] +name = "docutils" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] @@ -300,37 +482,44 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "fastmcp" -version = "2.3.4" +version = "2.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, - { name = "typer" }, + { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/d9/cc3eb61c59fec834a9492ea21df134381b4be76c35faa18cd2b0249278b8/fastmcp-2.3.4.tar.gz", hash = "sha256:f3fe004b8735b365a65ec2547eeb47db8352d5613697254854bc7c9c3c360eea", size = 998315 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/7a/4c6375a56f7458a4a6af62f4c4838a2c957a665cf5edad26fe95395666f1/fastmcp-2.13.2.tar.gz", hash = "sha256:2a206401a6579fea621974162674beba85b467ad72c70c1a3752a31951dff7f0", size = 8185950, upload-time = "2025-12-01T18:48:16.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e6/310d1fe6708b7338e1f48915a13d8bf00fd0599acdc7bf98da4fd20fcb66/fastmcp-2.3.4-py3-none-any.whl", hash = "sha256:12a45f72dd95aeaa1a6a56281fff96ca46929def3ccd9f9eb125cb97b722fbab", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/e5/4b/73c68b0ae9e587f20c5aa13ba5bed9be2bb9248a598555dafcf17df87f70/fastmcp-2.13.2-py3-none-any.whl", hash = "sha256:300c59eb970c235bb9d0575883322922e4f2e2468a3d45e90cbfd6b23b7be245", size = 385643, upload-time = "2025-12-01T18:48:18.515Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] @@ -341,18 +530,18 @@ dependencies = [ { name = "click" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/21/58251b3de99e0b5ba649ff511f7f9e8399c3059dd52a643774106e929afa/geomet-0.2.1.post1.tar.gz", hash = "sha256:91d754f7c298cbfcabd3befdb69c641c27fe75e808b27aa55028605761d17e95", size = 19728 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/21/58251b3de99e0b5ba649ff511f7f9e8399c3059dd52a643774106e929afa/geomet-0.2.1.post1.tar.gz", hash = "sha256:91d754f7c298cbfcabd3befdb69c641c27fe75e808b27aa55028605761d17e95", size = 19728, upload-time = "2020-01-12T00:07:21.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/81/156ca48f950f833ddc392f8e3677ca50a18cb9d5db38ccb4ecea55a9303f/geomet-0.2.1.post1-py3-none-any.whl", hash = "sha256:a41a1e336b381416d6cbed7f1745c848e91defaa4d4c1bdc1312732e46ffad2b", size = 18462 }, + { url = "https://files.pythonhosted.org/packages/c9/81/156ca48f950f833ddc392f8e3677ca50a18cb9d5db38ccb4ecea55a9303f/geomet-0.2.1.post1-py3-none-any.whl", hash = "sha256:a41a1e336b381416d6cbed7f1745c848e91defaa4d4c1bdc1312732e46ffad2b", size = 18462, upload-time = "2020-01-12T00:07:18.431Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -363,9 +552,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, + { 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]] @@ -378,45 +567,45 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, ] [[package]] name = "identify" version = "2.6.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101 }, + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] @@ -426,18 +615,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jmespath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] [[package]] @@ -450,9 +639,24 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 }, + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, ] [[package]] @@ -462,9 +666,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] @@ -475,9 +679,9 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "win32-setctime", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] [[package]] @@ -487,42 +691,42 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, ] [[package]] name = "mcp" -version = "1.11.0" +version = "1.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -531,33 +735,36 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907 } +sdist = { url = "https://files.pythonhosted.org/packages/12/42/10c0c09ca27aceacd8c428956cfabdd67e3d328fe55c4abc16589285d294/mcp-1.23.1.tar.gz", hash = "sha256:7403e053e8e2283b1e6ae631423cb54736933fea70b32422152e6064556cd298", size = 596519, upload-time = "2025-12-02T18:41:12.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880 }, + { url = "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl", hash = "sha256:3ce897fcc20a41bd50b4c58d3aa88085f11f505dcc0eaed48930012d34c731d8", size = 231433, upload-time = "2025-12-02T18:41:11.195Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -567,36 +774,54 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 } +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 }, + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -610,9 +835,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] @@ -622,14 +847,58 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +memory = [ + { name = "cachetools" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] name = "pydantic" -version = "2.11.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -637,65 +906,72 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900 }, + { 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" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { 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/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/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { 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" }, ] [[package]] @@ -707,18 +983,41 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] [[package]] @@ -729,9 +1028,9 @@ dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 }, + { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" }, ] [[package]] @@ -746,9 +1045,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -758,9 +1057,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, ] [[package]] @@ -771,9 +1070,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, ] [[package]] @@ -783,9 +1082,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814, upload-time = "2024-03-21T22:14:04.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863, upload-time = "2024-03-21T22:14:02.694Z" }, ] [[package]] @@ -795,27 +1094,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] [[package]] @@ -823,38 +1122,38 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, ] [[package]] @@ -864,9 +1163,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "prompt-toolkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775, upload-time = "2024-12-29T11:49:17.802Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 }, + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747, upload-time = "2024-12-29T11:49:16.734Z" }, ] [[package]] @@ -878,9 +1177,24 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { 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" } +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" }, ] [[package]] @@ -892,92 +1206,105 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, ] [[package]] name = "rpds-py" version = "0.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 }, - { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 }, - { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 }, - { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 }, - { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 }, - { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 }, - { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 }, - { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 }, - { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 }, - { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 }, - { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 }, - { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 }, - { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 }, - { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 }, - { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 }, - { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 }, - { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 }, - { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 }, - { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 }, - { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 }, - { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 }, - { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 }, - { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 }, - { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 }, - { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 }, - { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 }, - { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 }, - { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 }, - { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 }, - { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 }, - { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 }, - { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 }, - { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 }, - { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 }, - { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 }, - { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 }, - { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 }, - { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 }, - { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 }, - { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 }, - { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 }, - { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 }, - { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 }, - { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 }, - { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 }, - { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 }, - { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 }, - { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 }, - { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466, upload-time = "2025-07-01T15:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825, upload-time = "2025-07-01T15:53:42.247Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530, upload-time = "2025-07-01T15:53:43.585Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933, upload-time = "2025-07-01T15:53:45.78Z" }, + { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973, upload-time = "2025-07-01T15:53:47.085Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293, upload-time = "2025-07-01T15:53:48.117Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787, upload-time = "2025-07-01T15:53:50.874Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312, upload-time = "2025-07-01T15:53:52.046Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403, upload-time = "2025-07-01T15:53:53.192Z" }, + { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323, upload-time = "2025-07-01T15:53:54.336Z" }, + { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541, upload-time = "2025-07-01T15:53:55.469Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442, upload-time = "2025-07-01T15:53:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314, upload-time = "2025-07-01T15:53:57.842Z" }, + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226, upload-time = "2025-07-01T15:56:16.578Z" }, + { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230, upload-time = "2025-07-01T15:56:17.978Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363, upload-time = "2025-07-01T15:56:19.977Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146, upload-time = "2025-07-01T15:56:21.39Z" }, + { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804, upload-time = "2025-07-01T15:56:22.78Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820, upload-time = "2025-07-01T15:56:24.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567, upload-time = "2025-07-01T15:56:26.064Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520, upload-time = "2025-07-01T15:56:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362, upload-time = "2025-07-01T15:56:29.078Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113, upload-time = "2025-07-01T15:56:30.485Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429, upload-time = "2025-07-01T15:56:31.956Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950, upload-time = "2025-07-01T15:56:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" }, ] [[package]] name = "ruff" version = "0.11.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632, upload-time = "2025-05-15T14:08:56.76Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243 }, - { url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636 }, - { url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624 }, - { url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358 }, - { url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850 }, - { url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787 }, - { url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479 }, - { url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760 }, - { url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747 }, - { url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657 }, - { url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671 }, - { url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135 }, - { url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179 }, - { url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021 }, - { url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958 }, - { url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285 }, - { url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278 }, + { url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243, upload-time = "2025-05-15T14:08:12.884Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636, upload-time = "2025-05-15T14:08:16.551Z" }, + { url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624, upload-time = "2025-05-15T14:08:19.032Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358, upload-time = "2025-05-15T14:08:21.542Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850, upload-time = "2025-05-15T14:08:23.682Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787, upload-time = "2025-05-15T14:08:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479, upload-time = "2025-05-15T14:08:28.013Z" }, + { url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760, upload-time = "2025-05-15T14:08:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747, upload-time = "2025-05-15T14:08:33.297Z" }, + { url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657, upload-time = "2025-05-15T14:08:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671, upload-time = "2025-05-15T14:08:38.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135, upload-time = "2025-05-15T14:08:41.247Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179, upload-time = "2025-05-15T14:08:43.762Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021, upload-time = "2025-05-15T14:08:46.451Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958, upload-time = "2025-05-15T14:08:49.601Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285, upload-time = "2025-05-15T14:08:52.392Z" }, + { url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" }, ] [[package]] @@ -987,36 +1314,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/9e/73b14aed38ee1f62cd30ab93cd0072dec7fb01f3033d116875ae3e7b8b44/s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c", size = 149178 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/64/d2b49620039b82688aeebd510bd62ff4cdcdb86cbf650cc72ae42c5254a3/s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18", size = 84773 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/9e/73b14aed38ee1f62cd30ab93cd0072dec7fb01f3033d116875ae3e7b8b44/s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c", size = 149178, upload-time = "2025-04-22T21:08:09.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/89/64/d2b49620039b82688aeebd510bd62ff4cdcdb86cbf650cc72ae42c5254a3/s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18", size = 84773, upload-time = "2025-04-22T21:08:08.265Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -1027,9 +1345,9 @@ dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511 } +sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233 }, + { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" }, ] [[package]] @@ -1039,105 +1357,90 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] [[package]] name = "termcolor" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057, upload-time = "2024-10-06T19:50:04.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755, upload-time = "2024-10-06T19:50:02.097Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "tomlkit" version = "0.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - -[[package]] -name = "typer" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258 }, + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, + { 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-inspection" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, + { 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" }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] [[package]] name = "uvicorn" -version = "0.34.2" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] [[package]] @@ -1149,62 +1452,62 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316 } +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 }, + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "win32-setctime" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] From eac2fb74f3995bd6f4eda397cec3912160c4f27b Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 3 Dec 2025 21:45:21 -0800 Subject: [PATCH 02/24] Improve model docstrings --- .../amazon_keyspaces_mcp_server/models.py | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py index 333a606f94..4bd95d7f76 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py @@ -19,7 +19,17 @@ @dataclass class KeyspaceInfo: - """Information about a Cassandra keyspace.""" + """Information about a Cassandra keyspace. + + This model represents metadata about a keyspace in Cassandra or Amazon Keyspaces, + including its replication configuration. + + Attributes: + name: The name of the keyspace. + replication_strategy: The replication strategy class (e.g., SimpleStrategy, + NetworkTopologyStrategy). + replication_factor: The number of replicas for the keyspace data. + """ name: str replication_strategy: str = '' @@ -28,7 +38,18 @@ class KeyspaceInfo: @dataclass class ColumnInfo: - """Information about a Cassandra column.""" + """Information about a Cassandra column. + + This model represents metadata about a column in a Cassandra table, + including its data type and role in the primary key structure. + + Attributes: + name: The name of the column. + type: The CQL data type of the column (e.g., text, int, uuid). + is_primary_key: Whether this column is part of the primary key. + is_partition_key: Whether this column is part of the partition key. + is_clustering_column: Whether this column is a clustering column. + """ name: str type: str @@ -39,7 +60,16 @@ class ColumnInfo: @dataclass class TableInfo: - """Information about a Cassandra table.""" + """Information about a Cassandra table. + + This model represents metadata about a table in Cassandra or Amazon Keyspaces, + including its columns and schema information. + + Attributes: + name: The name of the table. + keyspace: The keyspace containing this table. + columns: List of column metadata for the table. + """ name: str keyspace: str @@ -48,7 +78,19 @@ class TableInfo: @dataclass class QueryResult: - """Result of a CQL query execution.""" + """Result of a CQL query execution. + + This model encapsulates the results returned from executing a CQL query, + including the data rows and execution metadata. + + Attributes: + columns: List of column names in the result set. + rows: List of result rows, where each row is a dictionary mapping + column names to values. + row_count: Total number of rows returned by the query. + execution_info: Additional execution metadata such as queried host + and performance metrics. + """ columns: List[str] rows: List[Dict[str, Any]] @@ -58,7 +100,22 @@ class QueryResult: @dataclass class QueryAnalysisResult: - """Result of a query performance analysis.""" + """Result of a query performance analysis. + + This model contains the analysis results for a CQL query, identifying + potential performance issues and providing optimization recommendations. + + Attributes: + query: The CQL query that was analyzed. + table_name: The name of the table being queried. + uses_partition_key: Whether the query filters on the partition key. + uses_clustering_columns: Whether the query uses clustering columns in WHERE clause. + uses_allow_filtering: Whether the query uses ALLOW FILTERING clause. + uses_secondary_index: Whether the query uses a secondary index. + is_full_table_scan: Whether the query requires a full table scan. + recommendations: List of optimization recommendations for the query. + performance_assessment: Overall assessment of the query's performance characteristics. + """ query: str table_name: str = '' From 63ad835b6d591da0f722603a4aa641d51ce4d244 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 3 Dec 2025 23:58:40 -0800 Subject: [PATCH 03/24] Remove non-callable tests of FunctionTool objects --- .../tests/test_server.py | 108 +----------------- 1 file changed, 2 insertions(+), 106 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index 0fce8ff168..9812c699ec 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -15,117 +15,13 @@ from awslabs.amazon_keyspaces_mcp_server.models import KeyspaceInfo, QueryAnalysisResult, TableInfo from awslabs.amazon_keyspaces_mcp_server.server import ( KeyspacesMcpStdioServer, - analyze_query_performance, - describe_keyspace, - describe_table, - execute_query, get_proxy, - list_keyspaces, - list_tables, ) from mcp.server.fastmcp import Context -from unittest.mock import AsyncMock, Mock, patch -class TestServerTools(unittest.TestCase): - """Tests for the server tool functions.""" - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_list_keyspaces(self, mock_get_proxy): - """Test the list_keyspaces tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy.handle_list_keyspaces.return_value = 'Keyspaces list' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = list_keyspaces() - - # Verify the result - self.assertEqual(result, 'Keyspaces list') - mock_proxy.handle_list_keyspaces.assert_called_once_with(None) - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_list_tables(self, mock_get_proxy): - """Test the list_tables tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy._handle_list_tables.return_value = 'Tables list' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = list_tables('mykeyspace') - - # Verify the result - self.assertEqual(result, 'Tables list') - mock_proxy._handle_list_tables.assert_called_once_with('mykeyspace', None) - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_describe_keyspace(self, mock_get_proxy): - """Test the describe_keyspace tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy._handle_describe_keyspace.return_value = 'Keyspace details' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = describe_keyspace('mykeyspace') - - # Verify the result - self.assertEqual(result, 'Keyspace details') - mock_proxy._handle_describe_keyspace.assert_called_once_with('mykeyspace', None) - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_describe_table(self, mock_get_proxy): - """Test the describe_table tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy._handle_describe_table.return_value = 'Table details' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = describe_table('mykeyspace', 'users') - - # Verify the result - self.assertEqual(result, 'Table details') - mock_proxy._handle_describe_table.assert_called_once_with('mykeyspace', 'users', None) - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_execute_query(self, mock_get_proxy): - """Test the execute_query tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy._handle_execute_query.return_value = 'Query results' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = execute_query('mykeyspace', 'SELECT * FROM users') - - # Verify the result - self.assertEqual(result, 'Query results') - mock_proxy._handle_execute_query.assert_called_once_with( - 'mykeyspace', 'SELECT * FROM users', None - ) - - @patch('awslabs.amazon_keyspaces_mcp_server.server.get_proxy') - def test_analyze_query_performance(self, mock_get_proxy): - """Test the analyze_query_performance tool.""" - # Set up the mock - mock_proxy = Mock() - mock_proxy._handle_analyze_query_performance.return_value = 'Query analysis' - mock_get_proxy.return_value = mock_proxy - - # Call the function - result = analyze_query_performance('mykeyspace', 'SELECT * FROM users') - - # Verify the result - self.assertEqual(result, 'Query analysis') - mock_proxy._handle_analyze_query_performance.assert_called_once_with( - 'mykeyspace', 'SELECT * FROM users', None - ) - - -class TestKeyspacesMcpStdioServer(unittest.TestCase): +# pylint: disable=protected-access,too-many-public-methods +class TestKeyspacesMcpStdioServer(unittest.IsolatedAsyncioTestCase): """Tests for the KeyspacesMcpStdioServer class.""" def setUp(self): From 1d8c4928576a64c72ea614c9c01e36c4e849d2c7 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 00:00:08 -0800 Subject: [PATCH 04/24] Fix pylint warnings/errors --- .../amazon_keyspaces_mcp_server/client.py | 100 +++++--- .../amazon_keyspaces_mcp_server/config.py | 5 +- .../amazon_keyspaces_mcp_server/exceptions.py | 40 ++++ .../llm_context.py | 223 ++++++++++++------ .../amazon_keyspaces_mcp_server/server.py | 139 ++++++----- .../amazon_keyspaces_mcp_server/services.py | 69 ++++-- .../pyproject.toml | 2 +- .../tests/test_server.py | 135 ++++++----- src/amazon-keyspaces-mcp-server/uv.lock | 2 +- 9 files changed, 459 insertions(+), 256 deletions(-) create mode 100644 src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py index ef72449b93..50ede24cc0 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py @@ -21,6 +21,12 @@ import logging import os import ssl +from typing import Any, Dict, List, Optional + +from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster, Session +from cassandra.io.asyncioreactor import AsyncioConnection + from .consts import ( CERT_DIRECTORY, CERT_FILENAME, @@ -28,13 +34,8 @@ CONTROL_CONNECTION_TIMEOUT, KEYSPACES_DEFAULT_PORT, PROTOCOL_VERSION, + UNSAFE_OPERATIONS, ) -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, Session - -# Use asyncore reactor for Python 3.11 compatibility -from cassandra.io.asyncorereactor import AsyncoreConnection -from typing import Any, Dict, List, Optional # Older versions of the Cassandra Python driver may not include SSLOptions. Conditionally @@ -80,8 +81,8 @@ def __init__(self, database_config: DatabaseConfig): logger.info('Connected to Cassandra cluster') except Exception as e: target = 'Amazon Keyspaces' if self.is_keyspaces else 'Cassandra cluster' - logger.error(f'Failed to connect to {target}: {str(e)}') - raise RuntimeError(f'Failed to connect to {target}: {str(e)}') + logger.error('Failed to connect to %s: %s', target, str(e)) + raise RuntimeError(f'Failed to connect to {target}: {str(e)}') from e def _create_cassandra_session(self) -> Session: """Create a session for Apache Cassandra.""" @@ -101,7 +102,7 @@ def _create_cassandra_session(self) -> Session: connect_timeout=int(CONNECTION_TIMEOUT), ) - cluster.connection_class = AsyncoreConnection + cluster.connection_class = AsyncioConnection return cluster.connect() @@ -143,7 +144,7 @@ def _create_keyspaces_session(self) -> Session: connect_timeout=int(CONNECTION_TIMEOUT), ) - cluster.connection_class = AsyncoreConnection + cluster.connection_class = AsyncioConnection return cluster.connect() @@ -155,9 +156,9 @@ def _create_ssl_context_for_keyspaces(self) -> ssl.SSLContext: try: ssl_context.load_verify_locations(cafile=cert_path) - logger.info(f'Loaded certificate from {cert_path}') - except Exception as e: - logger.error(f'Failed to load certificate from {cert_path}: {str(e)}') + logger.info('Loaded certificate from %s', cert_path) + except (FileNotFoundError, ssl.SSLError, OSError) as e: + logger.error('Failed to load certificate from %s: %s', cert_path, str(e)) # Fall back to default CA certs, and best of luck ssl_context.load_default_certs() @@ -194,9 +195,9 @@ def list_keyspaces(self) -> List[KeyspaceInfo]: keyspaces.append(keyspace_info) return keyspaces - except Exception as e: - logger.error(f'Error listing keyspaces: {str(e)}') - raise RuntimeError(f'Failed to list keyspaces: {str(e)}') + except (RuntimeError, ValueError) as e: + logger.error('Error listing keyspaces: %s', str(e)) + raise RuntimeError(f'Failed to list keyspaces: {str(e)}') from e def list_tables(self, keyspace_name: str) -> List[TableInfo]: """List all tables in a keyspace.""" @@ -211,9 +212,11 @@ def list_tables(self, keyspace_name: str) -> List[TableInfo]: tables.append(TableInfo(name=name, keyspace=keyspace_name)) return tables - except Exception as e: - logger.error(f'Error listing tables for keyspace {keyspace_name}: {str(e)}') - raise RuntimeError(f'Failed to list tables for keyspace {keyspace_name}: {str(e)}') + except (RuntimeError, ValueError) as e: + logger.error('Error listing tables for keyspace %s: %s', keyspace_name, str(e)) + raise RuntimeError( + f'Failed to list tables for keyspace {keyspace_name}: {str(e)}' + ) from e def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: """Get detailed information about a keyspace.""" @@ -238,15 +241,16 @@ def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: self._add_keyspaces_context(keyspace_details) return keyspace_details - except Exception as e: - logger.error(f'Error describing keyspace {keyspace_name}: {str(e)}') - raise RuntimeError(f'Failed to describe keyspace {keyspace_name}: {str(e)}') + except (RuntimeError, ValueError) as e: + logger.error('Error describing keyspace %s: %s', keyspace_name, str(e)) + raise RuntimeError(f'Failed to describe keyspace {keyspace_name}: {str(e)}') from e def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: """Get detailed information about a table.""" try: query = ( - 'SELECT * FROM system_schema.tables WHERE keyspace_name = %s AND table_name = %s' + 'SELECT * FROM system_schema.tables WHERE ' + 'keyspace_name = %s AND table_name = %s' ) table_row = self.session.execute(query, [keyspace_name, table_name]).one() @@ -299,8 +303,13 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: # Add capacity mode information for Keyspaces tables try: - query = 'SELECT custom_properties FROM system_schema_mcs.tables WHERE keyspace_name = %s AND table_name = %s' - capacity_row = self.session.execute(query, [keyspace_name, table_name]).one() + query = ( + 'SELECT custom_properties FROM system_schema_mcs.tables ' + 'WHERE keyspace_name = %s AND table_name = %s' + ) + capacity_row = self.session.execute( + query, [keyspace_name, table_name] + ).one() if capacity_row and capacity_row.custom_properties: props = capacity_row.custom_properties @@ -314,16 +323,21 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: table_details['write_capacity_units'] = int( props.get('write_capacity_units', 0) ) - except Exception as e: + except (RuntimeError, ValueError, AttributeError) as e: # Ignore errors when trying to get capacity information logger.warning( - f'Could not retrieve capacity information for table: {keyspace_name}.{table_name}: {str(e)}' + 'Could not retrieve capacity information for table: %s.%s: %s', + keyspace_name, + table_name, + str(e), ) return table_details - except Exception as e: - logger.error(f'Error describing table {keyspace_name}.{table_name}: {str(e)}') - raise RuntimeError(f'Failed to describe table {keyspace_name}.{table_name}: {str(e)}') + except (RuntimeError, ValueError) as e: + logger.error('Error describing table %s.%s: %s', keyspace_name, table_name, str(e)) + raise RuntimeError( + f'Failed to describe table {keyspace_name}.{table_name}: {str(e)}' + ) from e def execute_read_only_query( self, query: str, params: Optional[List[Any]] = None @@ -337,12 +351,12 @@ def execute_read_only_query( # Check for any modifications that might be disguised as SELECT if any( op in trimmed_query - for op in ['insert ', 'update ', 'delete ', 'drop ', 'truncate ', 'create ', 'alter '] + for op in UNSAFE_OPERATIONS ): raise ValueError('Query contains potentially unsafe operations') try: - logger.info(f'Executing read-only query: {query}') + logger.info('Executing read-only query: %s', query) # Execute the query if params: @@ -367,8 +381,10 @@ def execute_read_only_query( try: if hasattr(row, col_name) and getattr(row, col_name) is not None: value = getattr(row, col_name) - except Exception as e: - logger.warning(f'Error getting value for column {col_name}: {str(e)}') + except (AttributeError, TypeError, ValueError) as e: + logger.warning( + 'Error getting value for column %s: %s', col_name, str(e) + ) row_data[col_name] = value rows.append(row_data) @@ -388,9 +404,9 @@ def execute_read_only_query( result['execution_info'] = execution_info return result - except Exception as e: - logger.error(f'Error executing query: {query}: {str(e)}') - raise RuntimeError(f'Failed to execute query: {str(e)}') + except (RuntimeError, ValueError) as e: + logger.error('Error executing query: %s: %s', query, str(e)) + raise RuntimeError(f'Failed to execute query: {str(e)}') from e def _add_keyspaces_context(self, details: Dict[str, Any]) -> None: """Add Keyspaces-specific context to the details.""" @@ -400,8 +416,14 @@ def _add_keyspaces_context(self, details: Dict[str, Any]) -> None: def _build_service_characteristics(self) -> Dict[str, Any]: """Build service characteristics for Amazon Keyspaces.""" characteristics: Dict[str, Any] = { - 'write_throughput_limitation': 'Amazon Keyspaces has specific throughput characteristics that differ from self-managed Cassandra', - 'implementation_notes': 'The service architecture imposes a 1MB item size limit and throughput constraints different from open-source Cassandra', + 'write_throughput_limitation': ( + 'Amazon Keyspaces has specific throughput characteristics that ' + 'differ from self-managed Cassandra' + ), + 'implementation_notes': ( + 'The service architecture imposes a 1MB item size limit and ' + 'throughput constraints different from open-source Cassandra' + ), } response_guidance = { diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py index 3f8d9a569f..7b39fc3d4e 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py @@ -14,10 +14,11 @@ """Configuration module for Keyspaces MCP Server.""" import os -from .consts import CASSANDRA_DEFAULT_PORT, ENV_DIRECTORY, ENV_FILENAME from dataclasses import dataclass from dotenv import load_dotenv +from .consts import CASSANDRA_DEFAULT_PORT, ENV_DIRECTORY, ENV_FILENAME + # Load environment variables from ENV_FILENAME in the user's home directory, # if it exists. @@ -48,7 +49,7 @@ def from_env(cls): return cls( use_keyspaces=os.getenv('DB_USE_KEYSPACES', 'false').lower() == 'true', cassandra_contact_points=os.getenv('DB_CASSANDRA_CONTACT_POINTS', '127.0.0.1'), - cassandra_port=int(os.getenv('DB_CASSANDRA_PORT', CASSANDRA_DEFAULT_PORT)), + cassandra_port=int(os.getenv('DB_CASSANDRA_PORT', str(CASSANDRA_DEFAULT_PORT))), cassandra_local_datacenter=os.getenv('DB_CASSANDRA_LOCAL_DATACENTER', 'datacenter1'), cassandra_username=os.getenv('DB_CASSANDRA_USERNAME', ''), cassandra_password=os.getenv('DB_CASSANDRA_PASSWORD', ''), diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py new file mode 100644 index 0000000000..ecc06d9550 --- /dev/null +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# 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. +"""Custom exceptions for Amazon Keyspaces MCP Server.""" + + +class KeyspacesException(Exception): + """Base exception for Keyspaces MCP server errors.""" + + def __init__(self, message: str, details: str = ""): + """Initialize exception with message and optional details.""" + self.message = message + self.details = details + super().__init__(self.message) + + +class ValidationError(KeyspacesException): + """Raised when input validation fails.""" + + +class QuerySecurityError(KeyspacesException): + """Raised when a query violates security constraints.""" + + +class QueryExecutionError(KeyspacesException): + """Raised when query execution fails.""" + + +class SchemaError(KeyspacesException): + """Raised when schema operations fail.""" diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py index cfc13f1dd7..83d8c2dc4a 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py @@ -13,11 +13,12 @@ # limitations under the License. """LLM context builder for Keyspaces MCP Server.""" -from .models import KeyspaceInfo, QueryAnalysisResult, TableInfo from typing import Any, Dict, List +from .models import KeyspaceInfo, QueryAnalysisResult, TableInfo + -def build_list_keyspaces_context(keyspaces: List[KeyspaceInfo]) -> str: +def build_list_keyspaces_context(_keyspaces: List[KeyspaceInfo]) -> str: """Provide LLM context for Amazon Keyspaces and Apache Cassandra.""" context = { 'cassandra_knowledge': build_cassandra_knowledge(), @@ -26,25 +27,39 @@ def build_list_keyspaces_context(keyspaces: List[KeyspaceInfo]) -> str: # Add keyspace-specific guidance list_keyspaces_guidance = { - 'compatibility': 'Amazon Keyspaces is compatible with Apache Cassandra 3.11. This means that it supports most ' - 'of the same CQL language features and is driver-protocol compatible with Cassandra 3.11.', - 'limitations': "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. Unsupported features " - 'include logged batches, materialized views, indexes, aggregate functions like COUNT and SUM, prepared ' - 'statements for DDL operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, the inequality operator ' - 'for user-defined types, or the IN keyword in INSERT and UPDATE statements. Keyspaces uses AWS IAM for ' - "authentication and authorization, and not Cassandra's security configuration and commands. Additionally, " - 'some operations that are synchronous in Cassandra are asynchronous in Keyspaces, such as DDL operations ' - 'and range delete operations.', - 'replication_strategy': 'In Cassandra, common replication strategies include SimpleStrategy and NetworkTopologyStrategy. ' - 'Amazon Keyspaces uses a single-region replication strategy with 3x replication for durability.', - 'naming_conventions': 'Keyspace names typically use snake_case and represent logical data domains.', + "compatibility": ( + "Amazon Keyspaces is compatible with Apache Cassandra 3.11. " + "This means that it supports most of the same CQL language features " + "and is driver-protocol compatible with Cassandra 3.11." + ), + "limitations": ( + "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " + "Unsupported features include logged batches, materialized views, " + "indexes, aggregate functions like COUNT and SUM, prepared statements " + "for DDL operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, " + "the inequality operator for user-defined types, or the IN keyword in " + "INSERT and UPDATE statements. Keyspaces uses AWS IAM for authentication " + "and authorization, and not Cassandra's security configuration and " + "commands. Additionally, some operations that are synchronous in Cassandra " + "are asynchronous in Keyspaces, such as DDL operations and range delete " + "operations." + ), + "replication_strategy": ( + "In Cassandra, common replication strategies include SimpleStrategy and " + "NetworkTopologyStrategy. Amazon Keyspaces uses a single-region " + "replication strategy with 3x replication for durability." + ), + "naming_conventions": ( + "Keyspace names typically use snake_case and represent logical data " + "domains." + ), } context['list_keyspaces_guidance'] = list_keyspaces_guidance return dict_to_markdown(context) -def build_list_tables_context(keyspace_name: str, tables: List[TableInfo]) -> str: +def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> str: """Provide LLM context for tables.""" context = { 'cassandra_knowledge': build_cassandra_knowledge(), @@ -53,20 +68,27 @@ def build_list_tables_context(keyspace_name: str, tables: List[TableInfo]) -> st # Add table-specific guidance tables_guidance = { - 'data_modeling': 'In Cassandra, tables are containers for related data, similar to tablesin relational databases. ' - 'However, Cassandra tables are optimized for specific access patterns based on their primary key ' - 'design. The primary key determines how data is distributed physically in the database, and the ' - 'attributes that can be specified for efficient query execution. Primary keys consist of a ' - 'partition key (which determines data distribution) and optional cluster columns which determine ' - 'how data is ordered within a partition.', - 'naming_conventions': 'Table names typically use snake_case and should be descriptive of the entity they represent.', + "data_modeling": ( + "In Cassandra, tables are containers for related data, similar to tables " + "in relational databases. However, Cassandra tables are optimized for " + "specific access patterns based on their primary key design. The primary " + "key determines how data is distributed physically in the database, and " + "the attributes that can be specified for efficient query execution. " + "Primary keys consist of a partition key (which determines data " + "distribution) and optional cluster columns which determine how data is " + "ordered within a partition." + ), + "naming_conventions": ( + "Table names typically use snake_case and should be descriptive of the " + "entity they represent." + ), } context['tables_guidance'] = tables_guidance return dict_to_markdown(context) -def build_keyspace_details_context(keyspace_details: Dict[str, Any]) -> str: +def build_keyspace_details_context(_keyspace_details: Dict[str, Any]) -> str: """Provide LLM context for keyspace details.""" context = { 'cassandra_knowledge': build_cassandra_knowledge(), @@ -75,10 +97,15 @@ def build_keyspace_details_context(keyspace_details: Dict[str, Any]) -> str: # Add keyspace-specific guidance keyspace_guidance = { - 'replication_strategy': 'Replication strategy determines how data is distributed across nodes. ' - 'Amazon Keyspaces manages replication automatically for high availability.', - 'durable_writes': 'Durable writes ensure data is written to the commit log before acknowledging the write. ' - 'This provides durability in case of node failures.', + "replication_strategy": ( + "Replication strategy determines how data is distributed across nodes. " + "Amazon Keyspaces manages replication automatically for high availability." + ), + "durable_writes": ( + "Durable writes ensure data is written to the commit log before " + "acknowledging the write. This provides durability in case of node " + "failures." + ), } context['keyspace_guidance'] = keyspace_guidance @@ -104,12 +131,19 @@ def build_table_details_context(table_details: Dict[str, Any]) -> str: # Add table-specific guidance table_guidance = { - 'partition_key': 'Partition keys determine data distribution across the cluster. ' - 'Queries are most efficient when they include the partition key.', - 'clustering_columns': 'Clustering columns determine the sort order within a partition. ' - 'They enable range queries within a partition.', - 'secondary_indexes': 'Secondary indexes should be used sparingly in Cassandra. ' - 'They are best for low-cardinality columns and can impact write performance.', + "partition_key": ( + "Partition keys determine data distribution across the cluster. " + "Queries are most efficient when they include the partition key." + ), + "clustering_columns": ( + "Clustering columns determine the sort order within a partition. " + "They enable range queries within a partition." + ), + "secondary_indexes": ( + "Secondary indexes should be used sparingly in Cassandra. " + "They are best for low-cardinality columns and can impact write " + "performance." + ), } context['table_guidance'] = table_guidance @@ -125,14 +159,20 @@ def build_query_result_context(query_results: Dict[str, Any]) -> str: # Add query-specific guidance query_guidance = { - 'performance_considerations': 'Cassandra queries are most efficient when they include the partition key. ' - 'Queries without a partition key may require a full table scan, which can be ' - 'inefficient for large tables.', - 'pagination': 'For large result sets, consider using pagination with the LIMIT clause and token-based paging ' - 'to avoid loading too many rows in memory.', - 'consistency_level': 'The consistency level determines how many replicas must acknowledge a read request ' - 'before returning data. Higher consistency levels provide stronger guarantees but may ' - 'increase latency.', + "performance_considerations": ( + "Cassandra queries are most efficient when they include the partition key. " + "Queries without a partition key may require a full table scan, which can " + "be inefficient for large tables." + ), + "pagination": ( + "For large result sets, consider using pagination with the LIMIT clause " + "and token-based paging to avoid loading too many rows in memory." + ), + "consistency_level": ( + "The consistency level determines how many replicas must acknowledge a " + "read request before returning data. Higher consistency levels provide " + "stronger guarantees but may increase latency." + ), } context['query_guidance'] = query_guidance @@ -141,14 +181,14 @@ def build_query_result_context(query_results: Dict[str, Any]) -> str: result_guidance = {} if row_count == 0: - result_guidance['empty_result'] = ( - 'No rows were returned. This could mean either no matching data exists ' - 'or the query conditions were too restrictive.' + result_guidance["empty_result"] = ( + "No rows were returned. This could mean either no matching data exists " + "or the query conditions were too restrictive." ) elif row_count > 100: - result_guidance['large_result'] = ( - 'A large number of rows were returned. Consider adding more specific ' - 'filtering conditions or using pagination for better performance.' + result_guidance["large_result"] = ( + "A large number of rows were returned. Consider adding more specific " + "filtering conditions or using pagination for better performance." ) context['result_guidance'] = result_guidance @@ -159,22 +199,35 @@ def build_query_result_context(query_results: Dict[str, Any]) -> str: def build_query_analysis_context(analysis_result: QueryAnalysisResult) -> str: """Provide LLM context for query analysis results.""" context: Dict[str, Any] = { - 'cassandra knowledge': build_cassandra_knowledge(), - 'amazon keyspaces knowledge': build_amazon_keyspaces_knowledge(), + 'cassandra_knowledge': build_cassandra_knowledge(), + 'amazon_keyspaces_knowledge': build_amazon_keyspaces_knowledge(), } # Add query performance guidance performance_guidance = { - 'Partition key importance': "In Cassandra/Keyspaces, queries that don't filter on partition key require scanning all partitions, " - + 'which is extremely expensive and should be avoided.', - 'clustering_column_usage': 'After partition keys, clustering columns should be used in WHERE clauses to further narrow down the data ' - + 'that needs to be read within a partition.', - 'allow_filtering_warning': 'The ALLOW FILTERING clause forces Cassandra to scan potentially all partitions, ' - + 'which is very inefficient and should be avoided in production.', - 'secondary_indexes': 'Secondary indexes in Cassandra are not as efficient as in relational databases. ' - + 'They still require reading from multiple partitions and should be used sparingly.', - 'full_table_scan': 'Full table scans in Cassandra are extremely expensive operations that should be avoided. ' - + 'Always design your data model and queries to avoid scanning entire tables.', + "partition_key_importance": ( + "In Cassandra/Keyspaces, queries that don't filter on partition key " + "require scanning all partitions, which is extremely expensive and should " + "be avoided." + ), + "clustering_column_usage": ( + "After partition keys, clustering columns should be used in WHERE clauses " + "to further narrow down the data that needs to be read within a partition." + ), + "allow_filtering_warning": ( + "The ALLOW FILTERING clause forces Cassandra to scan potentially all " + "partitions, which is very inefficient and should be avoided in production." + ), + "secondary_indexes": ( + "Secondary indexes in Cassandra are not as efficient as in relational " + "databases. They still require reading from multiple partitions and should " + "be used sparingly." + ), + "full_table_scan": ( + "Full table scans in Cassandra are extremely expensive operations that " + "should be avoided. Always design your data model and queries to avoid " + "scanning entire tables." + ), } context['performance_guidance'] = performance_guidance @@ -196,14 +249,23 @@ def build_query_analysis_context(analysis_result: QueryAnalysisResult) -> str: def build_cassandra_knowledge() -> Dict[str, str]: """Provide general Cassandra knowledge.""" knowledge = { - 'data_model': 'Cassandra uses a wide-column store data model optimized for write performance and horizontal ' - 'scalability.', - 'query_patterns': 'Cassandra is optimized for high write throughput and queries that specify the partition key.', - 'limitations': 'Cassandra has limited support for joins, aggregations, and transactions. ' - 'Data modeling should denormalize data to support specific query patterns.', - 'keyspaces_vs_cassandra': 'Amazon Keyspaces is a managed Cassandra-compatible service with some differences ' - 'in performance characteristics and feature support compared to self-managed' - 'Cassandra.', + "data_model": ( + "Cassandra uses a wide-column store data model optimized for write " + "performance and horizontal scalability." + ), + "query_patterns": ( + "Cassandra is optimized for high write throughput and queries that " + "specify the partition key." + ), + "limitations": ( + "Cassandra has limited support for joins, aggregations, and transactions. " + "Data modeling should denormalize data to support specific query patterns." + ), + "keyspaces_vs_cassandra": ( + "Amazon Keyspaces is a managed Cassandra-compatible service with some " + "differences in performance characteristics and feature support compared " + "to self-managed Cassandra." + ), } return knowledge @@ -212,16 +274,23 @@ def build_cassandra_knowledge() -> Dict[str, str]: def build_amazon_keyspaces_knowledge() -> Dict[str, str]: """Provide Amazon Keyspaces specific knowledge.""" knowledge = { - 'compatibility': 'Amazon Keyspaces is compatible with Apache Cassandra 3.11. This means that it supports most ' - 'of the same CQL language features and is driver-protocol compatible with Cassandra 3.11.', - 'differences_from_cassandra': "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. Unsupported " - 'features include logged batches, materialized views, indexes, aggregate functions like COUNT and' - 'SUM, prepared statements for DDL operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions,' - 'the inequality operator ' - 'for user-defined types, or the IN keyword in INSERT and UPDATE statements. Keyspaces uses AWS IAM for ' - "authentication and authorization, and not Cassandra's security configuration and commands. Additionally, " - 'some operations that are synchronous in Cassandra are asynchronous in Keyspaces, such as DDL operations ' - 'and range delete operations.', + "compatibility": ( + "Amazon Keyspaces is compatible with Apache Cassandra 3.11. This means " + "that it supports most of the same CQL language features and is " + "driver-protocol compatible with Cassandra 3.11." + ), + "differences_from_cassandra": ( + "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " + "Unsupported features include logged batches, materialized views, indexes, " + "aggregate functions like COUNT and SUM, prepared statements for DDL " + "operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, the " + "inequality operator for user-defined types, or the IN keyword in INSERT " + "and UPDATE statements. Keyspaces uses AWS IAM for authentication and " + "authorization, and not Cassandra's security configuration and commands. " + "Additionally, some operations that are synchronous in Cassandra are " + "asynchronous in Keyspaces, such as DDL operations and range delete " + "operations." + ), } return knowledge diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 1d1a9222dc..543cf35a51 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -14,6 +14,12 @@ """awslabs MCP Server implementation for Amazon Keyspaces (for Apache Cassandra).""" import sys +from typing import Any, Optional + +from fastmcp import Context, FastMCP +from loguru import logger +from pydantic import Field + from .client import UnifiedCassandraClient from .config import AppConfig from .consts import ( @@ -22,6 +28,12 @@ SERVER_VERSION, UNSAFE_OPERATIONS, ) +from .exceptions import ( + QueryExecutionError, + QuerySecurityError, + SchemaError, + ValidationError, +) from .llm_context import ( build_keyspace_details_context, build_list_keyspaces_context, @@ -31,10 +43,6 @@ build_table_details_context, ) from .services import DataService, QueryAnalysisService, SchemaService -from loguru import logger -from fastmcp import Context, FastMCP -from pydantic import Field -from typing import Any, Optional # Remove all default handlers then add our own @@ -44,7 +52,7 @@ mcp = FastMCP(name=SERVER_NAME, version=SERVER_VERSION) # Global handle to hold the proxy to the specific db client -_proxy = None +_PROXY = None def get_proxy(): @@ -52,8 +60,8 @@ def get_proxy(): The singleton is initialized lazily. """ - global _proxy - if _proxy is None: + global _PROXY # pylint: disable=global-statement + if _PROXY is None: # Load configuration app_config = AppConfig.from_env() @@ -65,83 +73,86 @@ def get_proxy(): schema_service = SchemaService(cassandra_client) query_analysis_service = QueryAnalysisService(cassandra_client, schema_service) - _proxy = KeyspacesMcpStdioServer(data_service, query_analysis_service, schema_service) + _PROXY = KeyspacesMcpStdioServer(data_service, query_analysis_service, schema_service) - return _proxy + return _PROXY @mcp.tool( name='listKeyspaces', description='Lists all keyspaces in the Cassandra/Keyspaces database - args: none', ) -def list_keyspaces( +async def list_keyspaces( ctx: Optional[Context] = None, ) -> str: """Lists all keyspaces in the Cassandra/Keyspaces database.""" - return get_proxy().handle_list_keyspaces(ctx) + return await get_proxy()._handle_list_keyspaces(ctx) # pylint: disable=protected-access @mcp.tool( name='listTables', description='Lists all tables in a specified keyspace - args: keyspace', ) -def list_tables( +async def list_tables( keyspace: str = Field(..., description='The keyspace to list tables from.'), ctx: Optional[Context] = None, ) -> str: """Lists all tables in a specified keyspace.""" - return get_proxy()._handle_list_tables(keyspace, ctx) + return await get_proxy()._handle_list_tables(keyspace, ctx) # pylint: disable=protected-access @mcp.tool( name='describeKeyspace', description='Gets detailed information about a keyspace - args: keyspace', ) -def describe_keyspace( +async def describe_keyspace( keyspace: str = Field(..., description='The keyspace to retrieve metadata for.'), ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a keyspace.""" - return get_proxy()._handle_describe_keyspace(keyspace, ctx) + return await get_proxy()._handle_describe_keyspace(keyspace, ctx) # pylint: disable=protected-access @mcp.tool( name='describeTable', description='Gets detailed information about a table - args: keyspace, table', ) -def describe_table( +async def describe_table( keyspace: str = Field(..., description='The keyspace containing the table'), table: str = Field(..., description='The name of the table to describe'), ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a table.""" - return get_proxy()._handle_describe_table(keyspace, table, ctx) + return await get_proxy()._handle_describe_table(keyspace, table, ctx) # pylint: disable=protected-access @mcp.tool( name='executeQuery', description='Executes a read-only SELECT query against the database - args: keyspace, query', ) -def execute_query( +async def execute_query( keyspace: str = Field(..., description='The keyspace to execute the query against'), query: str = Field(..., description='The CQL SELECT query to execute'), ctx: Optional[Context] = None, ) -> str: """Executes a read-only (SELECT) query against the database.""" - return get_proxy()._handle_execute_query(keyspace, query, ctx) + return await get_proxy()._handle_execute_query(keyspace, query, ctx) # pylint: disable=protected-access @mcp.tool( name='analyzeQueryPerformance', description='Analyzes the performance characteristics of a CQL query - args: keyspace, query', ) -def analyze_query_performance( +async def analyze_query_performance( keyspace: str = Field(..., description='The keyspace to analyze the query against'), query: str = Field(..., description='The CQL query to analyze for performance'), ctx: Optional[Context] = None, ) -> str: """Analyzes the performance characteristics of a CQL query.""" - return get_proxy()._handle_analyze_query_performance(keyspace, query, ctx) + proxy = get_proxy() + return await proxy._handle_analyze_query_performance( # pylint: disable=protected-access + keyspace, query, ctx + ) class KeyspacesMcpStdioServer: @@ -158,7 +169,7 @@ def __init__( self.query_analysis_service = query_analysis_service self.schema_service = schema_service - def handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: + async def _handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: """Handle the listKeyspaces tool.""" try: keyspaces = self.schema_service.list_keyspaces() @@ -174,19 +185,21 @@ def handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: # Add contextual information about Cassandra/Keyspaces if ctx: - ctx.info('Adding contextual information about Cassandra/Keyspaces') # type: ignore[unused-coroutine] + ctx.info( + 'Adding contextual information about Cassandra/Keyspaces' + ) # type: ignore[unused-coroutine] formatted_text += build_list_keyspaces_context(keyspaces) return formatted_text except Exception as e: logger.error(f'Error listing keyspaces: {str(e)}') - raise Exception(f'Error listing keyspaces: {str(e)}') + raise SchemaError(f'Error listing keyspaces: {str(e)}') from e - def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None) -> str: + async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None) -> str: """Handle the listTables tool.""" try: if not keyspace: - raise Exception('Keyspace name is required') + raise ValidationError('Keyspace name is required') tables = self.schema_service.list_tables(keyspace) @@ -201,19 +214,23 @@ def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None) -> s # Add contextual information about tables in Cassandra if ctx: - ctx.info(f'Adding contextual information about tables in keyspace {keyspace}') # type: ignore[unused-coroutine] + ctx.info( + f'Adding contextual information about tables in keyspace {keyspace}' + ) # type: ignore[unused-coroutine] formatted_text += build_list_tables_context(keyspace, tables) return formatted_text except Exception as e: logger.error(f'Error listing tables: {str(e)}') - raise Exception(f'Error listing tables: {str(e)}') + raise SchemaError(f'Error listing tables: {str(e)}') from e - def _handle_describe_keyspace(self, keyspace: str, ctx: Optional[Context] = None) -> str: + async def _handle_describe_keyspace( + self, keyspace: str, ctx: Optional[Context] = None + ) -> str: """Handle the describeKeyspace tool.""" try: if not keyspace: - raise Exception('Keyspace name is required') + raise ValidationError('Keyspace name is required') keyspace_details = self.schema_service.describe_keyspace(keyspace) @@ -227,7 +244,8 @@ def _handle_describe_keyspace(self, keyspace: str, ctx: Optional[Context] = None # Add replication factor or datacenter details if 'SimpleStrategy' in replication.get('class', ''): - formatted_text += f'- **Replication Factor**: `{replication.get("replication_factor", "Unknown")}`\n' + rf = replication.get("replication_factor", "Unknown") + formatted_text += f'- **Replication Factor**: `{rf}`\n' elif 'NetworkTopologyStrategy' in replication.get('class', ''): formatted_text += '- **Datacenter Replication**:\n' for dc, factor in replication.items(): @@ -240,24 +258,26 @@ def _handle_describe_keyspace(self, keyspace: str, ctx: Optional[Context] = None # Add contextual information about replication strategies if ctx: - ctx.info('Adding contextual information about replication strategies') # type: ignore[unused-coroutine] + ctx.info( + 'Adding contextual information about replication strategies' + ) # type: ignore[unused-coroutine] formatted_text += build_keyspace_details_context(keyspace_details) return formatted_text except Exception as e: logger.error(f'Error describing keyspace: {str(e)}') - raise Exception(f'Error describing keyspace: {str(e)}') + raise SchemaError(f'Error describing keyspace: {str(e)}') from e - def _handle_describe_table( + async def _handle_describe_table( self, keyspace: str, table: str, ctx: Optional[Context] = None ) -> str: """Handle the describeTable tool.""" try: if not keyspace: - raise Exception('Keyspace name is required') + raise ValidationError('Keyspace name is required') if not table: - raise Exception('Table name is required') + raise ValidationError('Table name is required') table_details = self.schema_service.describe_table(keyspace, table) @@ -307,34 +327,35 @@ def _handle_describe_table( # Add contextual information about Cassandra data types and primary keys if ctx: ctx.info( - 'Adding contextual information about Cassandra data types and primary keys' + 'Adding contextual information about Cassandra data types and ' + 'primary keys' ) # type: ignore[unused-coroutine] formatted_text += build_table_details_context(table_details) return formatted_text except Exception as e: logger.error(f'Error describing table: {str(e)}') - raise Exception(f'Error describing table: {str(e)}') + raise SchemaError(f'Error describing table: {str(e)}') from e - def _handle_execute_query( + async def _handle_execute_query( self, keyspace: str, query: str, ctx: Optional[Context] = None ) -> str: """Handle the executeQuery tool.""" try: if not keyspace: - raise Exception('Keyspace name is required') + raise ValidationError('Keyspace name is required') if not query: - raise Exception('Query is required') + raise ValidationError('Query is required') # Validate that this is a read-only query trimmed_query = query.strip().lower() if not trimmed_query.startswith('select '): - raise Exception('Only SELECT queries are allowed for read-only execution') + raise QuerySecurityError('Only SELECT queries are allowed for read-only execution') # Check for any modifications that might be disguised as SELECT if any(op in trimmed_query for op in UNSAFE_OPERATIONS): - raise Exception('Query contains potentially unsafe operations') + raise QuerySecurityError('Query contains potentially unsafe operations') # Execute the query using the DataService query_results = self.data_service.execute_read_only_query(keyspace, query) @@ -369,34 +390,38 @@ def _handle_execute_query( # Add note if results were truncated if len(rows) > display_limit: - formatted_text += f'\n_Note: Showing {display_limit} of {len(rows)} total rows. Use LIMIT in your query to restrict results._' + note = ( + f'\n_Note: Showing {display_limit} of {len(rows)} total rows. ' + 'Use LIMIT in your query to restrict results._' + ) + formatted_text += note else: formatted_text += 'No rows returned.' # Add contextual information about CQL queries if ctx: - ctx.info('Adding contextual information about CQL queries') # type: ignore[unused-coroutine] + ctx.info( + 'Adding contextual information about CQL queries' + ) # type: ignore[unused-coroutine] formatted_text += build_query_result_context(query_results) return formatted_text - except ValueError as e: - # This is thrown for non-SELECT queries - logger.warning(f'Invalid query attempt: {str(e)}') - raise Exception(str(e)) + except (ValidationError, QuerySecurityError): + raise except Exception as e: logger.error(f'Error executing query: {str(e)}') - raise Exception(f'Error executing query: {str(e)}') + raise QueryExecutionError(f'Error executing query: {str(e)}') from e - def _handle_analyze_query_performance( + async def _handle_analyze_query_performance( self, keyspace: str, query: str, ctx: Optional[Context] = None ) -> str: """Handle the analyzeQueryPerformance tool.""" try: if not keyspace: - raise Exception('Keyspace name is required') + raise ValidationError('Keyspace name is required') if not query: - raise Exception('Query is required') + raise ValidationError('Query is required') analysis_result = self.query_analysis_service.analyze_query(keyspace, query) @@ -414,13 +439,17 @@ def _handle_analyze_query_performance( # Add contextual information about query performance in Cassandra if ctx: - ctx.info('Adding contextual information about query performance in Cassandra') # type: ignore[unused-coroutine] + ctx.info( + 'Adding contextual information about query performance in Cassandra' + ) # type: ignore[unused-coroutine] formatted_text += build_query_analysis_context(analysis_result) return formatted_text + except ValidationError: + raise except Exception as e: logger.error(f'Error analyzing query: {str(e)}') - raise Exception(f'Error analyzing query: {str(e)}') + raise QueryExecutionError(f'Error analyzing query: {str(e)}') from e def main(): diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py index 74cce9480b..f5265b784b 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py @@ -15,9 +15,10 @@ import logging import re +from typing import Any, Dict, List + from .client import UnifiedCassandraClient from .models import KeyspaceInfo, QueryAnalysisResult, TableInfo -from typing import Any, Dict, List logger = logging.getLogger(__name__) @@ -30,12 +31,13 @@ def __init__(self, cassandra_client: UnifiedCassandraClient): """Initialize the service with the given client.""" self.cassandra_client = cassandra_client logger.info( - f'SchemaService initialized. Using Keyspaces: {cassandra_client.is_using_keyspaces()}' + 'SchemaService initialized. Using Keyspaces: %s', + cassandra_client.is_using_keyspaces(), ) def execute_read_only_query(self, keyspace_name: str, query: str) -> Dict[str, Any]: """Execute a read-only SELECT query against the database.""" - logger.info(f'Executing read-only query on keyspace {keyspace_name}: {query}') + logger.info('Executing read-only query on keyspace %s: %s', keyspace_name, query) # If keyspace is specified, qualify the query with the keyspace full_query = query @@ -80,7 +82,8 @@ def __init__(self, cassandra_client: UnifiedCassandraClient): """Initialize the service with the given client.""" self.cassandra_client = cassandra_client logger.info( - f'SchemaService initialized. Using Keyspaces: {cassandra_client.is_using_keyspaces()}' + 'SchemaService initialized. Using Keyspaces: %s', + cassandra_client.is_using_keyspaces(), ) def list_keyspaces(self) -> List[KeyspaceInfo]: @@ -90,17 +93,17 @@ def list_keyspaces(self) -> List[KeyspaceInfo]: def list_tables(self, keyspace_name: str) -> List[TableInfo]: """List all tables in a keyspace.""" - logger.info(f'Listing tables for keyspace: {keyspace_name}') + logger.info('Listing tables for keyspace: %s', keyspace_name) return self.cassandra_client.list_tables(keyspace_name) def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: """Get detailed information about a keyspace.""" - logger.info(f'Describing keyspace: {keyspace_name}') + logger.info('Describing keyspace: %s', keyspace_name) return self.cassandra_client.describe_keyspace(keyspace_name) def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: """Get detailed information about a table.""" - logger.info(f'Describing table: {keyspace_name}.{table_name}') + logger.info('Describing table: %s.%s', keyspace_name, table_name) return self.cassandra_client.describe_table(keyspace_name, table_name) @@ -115,12 +118,13 @@ def __init__(self, cassandra_client: UnifiedCassandraClient, schema_service: Sch def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: """Analyze a CQL query for performance characteristics.""" - logger.info(f'Analyzing query for keyspace {keyspace_name}: {query}') + logger.info('Analyzing query for keyspace %s: %s', keyspace_name, query) result = QueryAnalysisResult(query=query) try: - # Normalize query for analysis (remove extra whitespace, convert to lowercase for pattern matching) + # Normalize query for analysis + # (remove extra whitespace, convert to lowercase for pattern matching) normalized_query = self._normalize_query(query) # Extract table name from the query @@ -187,8 +191,8 @@ def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: ) return result - except Exception as e: - logger.error(f'Error analyzing query: {str(e)}') + except Exception as e: # pylint: disable=broad-exception-caught + logger.error('Error analyzing query: %s', str(e)) result.performance_assessment = f'Error analyzing query: {str(e)}' return result @@ -319,15 +323,20 @@ def _generate_performance_assessment( if not result.uses_partition_key: assessment.append( 'HIGH COST QUERY: This query does not filter on all partition key columns. ' - 'It will require scanning multiple partitions, which is expensive in Cassandra/Keyspaces.\n' + 'It will require scanning multiple partitions, ' + 'which is expensive in Cassandra/Keyspaces.\n' ) result.recommendations.append( - f'Include all partition key columns in your WHERE clause: {", ".join(partition_key_columns)}' + ( + 'Include all partition key columns in your WHERE clause: ' + f'{", ".join(partition_key_columns)}' + ) ) else: assessment.append( - 'EFFICIENT PARTITION KEY USAGE: This query correctly filters on all partition key columns, ' + 'EFFICIENT PARTITION KEY USAGE: This query correctly filters ' + 'on all partition key columns, ' 'which allows Cassandra to efficiently locate the relevant data partitions.\n' ) @@ -335,11 +344,15 @@ def _generate_performance_assessment( if not result.uses_clustering_columns and clustering_columns: assessment.append( 'POTENTIAL OPTIMIZATION: This query does not filter on any clustering columns. ' - 'Adding filters on clustering columns can further improve performance by reducing the amount of data read within partitions.\n' + 'Adding filters on clustering columns can further improve ' + 'performance by reducing the amount of data read within partitions.\n' ) result.recommendations.append( - f'Consider adding filters on clustering columns when possible: {", ".join(clustering_columns)}' + ( + 'Consider adding filters on clustering columns when possible: ' + f'{", ".join(clustering_columns)}' + ) ) elif result.uses_clustering_columns: assessment.append( @@ -350,13 +363,17 @@ def _generate_performance_assessment( # Assess based on ALLOW FILTERING usage if result.uses_allow_filtering: assessment.append( - 'WARNING - ALLOW FILTERING: This query uses ALLOW FILTERING, which can be extremely expensive ' + 'WARNING - ALLOW FILTERING: This query uses ALLOW FILTERING, ' + 'which can be extremely expensive ' 'as it may force Cassandra to scan and filter large amounts of data.\n' ) result.recommendations.append('Avoid using ALLOW FILTERING in production environments') result.recommendations.append( - 'Consider creating a secondary index for the filtered columns or redesign your data model' + ( + 'Consider creating a secondary index for the filtered columns ' + 'or redesign your data model' + ) ) # Assess based on secondary index usage @@ -371,19 +388,27 @@ def _generate_performance_assessment( 'Monitor the performance of queries using secondary indexes' ) result.recommendations.append( - 'Consider denormalizing your data model instead of relying on secondary indexes for frequently used queries' + ( + 'Consider denormalizing your data model instead of relying on ' + 'secondary indexes for frequently used queries' + ) ) # Assess based on full table scan if result.is_full_table_scan: assessment.append( - 'CRITICAL PERFORMANCE ISSUE - FULL TABLE SCAN: This query will perform a full table scan, ' - 'which is extremely expensive in Cassandra/Keyspaces and should be avoided in production.\n' + 'CRITICAL PERFORMANCE ISSUE - FULL TABLE SCAN: ' + 'This query will perform a full table scan, ' + 'which is extremely expensive in Cassandra/Keyspaces ' + 'and should be avoided in production.\n' ) result.recommendations.append('Redesign your query to include partition key filters') result.recommendations.append( - 'Consider creating a materialized view or a new table with a different primary key structure' + ( + 'Consider creating a materialized view or a new table ' + 'with a different primary key structure' + ) ) result.performance_assessment = '\n'.join(assessment) diff --git a/src/amazon-keyspaces-mcp-server/pyproject.toml b/src/amazon-keyspaces-mcp-server/pyproject.toml index 5d61e09b94..db15df3e05 100644 --- a/src/amazon-keyspaces-mcp-server/pyproject.toml +++ b/src/amazon-keyspaces-mcp-server/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10,<3.12" dependencies = [ "boto3>=1.37.27", - "cassandra-driver>=3.25.0", + "cassandra-driver>=3.29.2", "fastmcp>=2.8.0", "loguru>=0.7.0", "mcp>=1.11.0", diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index 9812c699ec..13f38ed9f4 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -1,18 +1,29 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Unit tests for the server module.""" import unittest +from unittest.mock import Mock, patch + +import pytest + from awslabs.amazon_keyspaces_mcp_server.consts import MAX_DISPLAY_ROWS -from awslabs.amazon_keyspaces_mcp_server.models import KeyspaceInfo, QueryAnalysisResult, TableInfo +from awslabs.amazon_keyspaces_mcp_server.models import ( + KeyspaceInfo, + QueryAnalysisResult, + TableInfo, +) from awslabs.amazon_keyspaces_mcp_server.server import ( KeyspacesMcpStdioServer, get_proxy, @@ -32,17 +43,18 @@ def setUp(self): self.server = KeyspacesMcpStdioServer( self.mock_data_service, self.mock_query_analysis_service, self.mock_schema_service ) - self.mock_context = AsyncMock(spec=Context) + self.mock_context = Mock(spec=Context) + self.mock_context.info = Mock(return_value=None) - def test_handle_list_keyspaces(self): - """Test the handle_list_keyspaces method.""" + async def test_handle_list_keyspaces(self): + """Test the _handle_list_keyspaces method.""" # Set up the mock keyspace1 = KeyspaceInfo(name='system') keyspace2 = KeyspaceInfo(name='mykeyspace') self.mock_schema_service.list_keyspaces.return_value = [keyspace1, keyspace2] # Call the method - result = self.server.handle_list_keyspaces(self.mock_context) + result = await self.server._handle_list_keyspaces(self.mock_context) # Verify the result self.assertIn('## Available Keyspaces', result) @@ -51,13 +63,13 @@ def test_handle_list_keyspaces(self): self.mock_schema_service.list_keyspaces.assert_called_once() self.mock_context.info.assert_called_once() - def test_handle_list_keyspaces_empty(self): - """Test the handle_list_keyspaces method with no keyspaces.""" + async def test_handle_list_keyspaces_empty(self): + """Test the _handle_list_keyspaces method with no keyspaces.""" # Set up the mock self.mock_schema_service.list_keyspaces.return_value = [] # Call the method - result = self.server.handle_list_keyspaces(self.mock_context) + result = await self.server._handle_list_keyspaces(self.mock_context) # Verify the result self.assertIn('## Available Keyspaces', result) @@ -65,19 +77,19 @@ def test_handle_list_keyspaces_empty(self): self.mock_schema_service.list_keyspaces.assert_called_once() self.mock_context.info.assert_called_once() - def test_handle_list_keyspaces_error(self): + async def test_handle_list_keyspaces_error(self): """Test the handle_list_keyspaces method with an error.""" # Set up the mock self.mock_schema_service.list_keyspaces.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server.handle_list_keyspaces(self.mock_context) + await self.server._handle_list_keyspaces(self.mock_context) self.assertIn('Error listing keyspaces', str(context.exception)) self.mock_schema_service.list_keyspaces.assert_called_once() - def test_handle_list_tables(self): + async def test_handle_list_tables(self): """Test the _handle_list_tables method.""" # Set up the mock table1 = TableInfo(name='users', keyspace='mykeyspace') @@ -85,7 +97,7 @@ def test_handle_list_tables(self): self.mock_schema_service.list_tables.return_value = [table1, table2] # Call the method - result = self.server._handle_list_tables('mykeyspace', self.mock_context) + result = await self.server._handle_list_tables('mykeyspace', self.mock_context) # Verify the result self.assertIn('## Tables in Keyspace `mykeyspace`', result) @@ -94,13 +106,13 @@ def test_handle_list_tables(self): self.mock_schema_service.list_tables.assert_called_once_with('mykeyspace') self.mock_context.info.assert_called_once() - def test_handle_list_tables_empty(self): + async def test_handle_list_tables_empty(self): """Test the _handle_list_tables method with no tables.""" # Set up the mock self.mock_schema_service.list_tables.return_value = [] # Call the method - result = self.server._handle_list_tables('mykeyspace', self.mock_context) + result = await self.server._handle_list_tables('mykeyspace', self.mock_context) # Verify the result self.assertIn('## Tables in Keyspace `mykeyspace`', result) @@ -108,19 +120,19 @@ def test_handle_list_tables_empty(self): self.mock_schema_service.list_tables.assert_called_once_with('mykeyspace') self.mock_context.info.assert_called_once() - def test_handle_list_tables_error(self): + async def test_handle_list_tables_error(self): """Test the _handle_list_tables method with an error.""" # Set up the mock self.mock_schema_service.list_tables.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_list_tables('mykeyspace', self.mock_context) + await self.server._handle_list_tables('mykeyspace', self.mock_context) self.assertIn('Error listing tables', str(context.exception)) self.mock_schema_service.list_tables.assert_called_once_with('mykeyspace') - def test_handle_describe_keyspace(self): + async def test_handle_describe_keyspace(self): """Test the _handle_describe_keyspace method.""" # Set up the mock keyspace_details = { @@ -131,7 +143,7 @@ def test_handle_describe_keyspace(self): self.mock_schema_service.describe_keyspace.return_value = keyspace_details # Call the method - result = self.server._handle_describe_keyspace('mykeyspace', self.mock_context) + result = await self.server._handle_describe_keyspace('mykeyspace', self.mock_context) # Verify the result self.assertIn('## Keyspace: `mykeyspace`', result) @@ -142,7 +154,7 @@ def test_handle_describe_keyspace(self): self.mock_schema_service.describe_keyspace.assert_called_once_with('mykeyspace') self.mock_context.info.assert_called_once() - def test_handle_describe_keyspace_simple_strategy(self): + async def test_handle_describe_keyspace_simple_strategy(self): """Test the _handle_describe_keyspace method with SimpleStrategy.""" # Set up the mock keyspace_details = { @@ -153,7 +165,7 @@ def test_handle_describe_keyspace_simple_strategy(self): self.mock_schema_service.describe_keyspace.return_value = keyspace_details # Call the method - result = self.server._handle_describe_keyspace('mykeyspace', self.mock_context) + result = await self.server._handle_describe_keyspace('mykeyspace', self.mock_context) # Verify the result self.assertIn('## Keyspace: `mykeyspace`', result) @@ -164,19 +176,19 @@ def test_handle_describe_keyspace_simple_strategy(self): self.mock_schema_service.describe_keyspace.assert_called_once_with('mykeyspace') self.mock_context.info.assert_called_once() - def test_handle_describe_keyspace_error(self): + async def test_handle_describe_keyspace_error(self): """Test the _handle_describe_keyspace method with an error.""" # Set up the mock self.mock_schema_service.describe_keyspace.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_describe_keyspace('mykeyspace', self.mock_context) + await self.server._handle_describe_keyspace('mykeyspace', self.mock_context) self.assertIn('Error describing keyspace', str(context.exception)) self.mock_schema_service.describe_keyspace.assert_called_once_with('mykeyspace') - def test_handle_describe_table(self): + async def test_handle_describe_table(self): """Test the _handle_describe_table method.""" # Set up the mock table_details = { @@ -193,7 +205,7 @@ def test_handle_describe_table(self): self.mock_schema_service.describe_table.return_value = table_details # Call the method - result = self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) + result = await self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) # Verify the result self.assertIn('## Table: `mykeyspace.users`', result) @@ -208,7 +220,7 @@ def test_handle_describe_table(self): self.mock_schema_service.describe_table.assert_called_once_with('mykeyspace', 'users') self.mock_context.info.assert_called_once() - def test_handle_describe_table_with_clustering_columns(self): + async def test_handle_describe_table_with_clustering_columns(self): """Test the _handle_describe_table method with clustering columns.""" # Set up the mock table_details = { @@ -225,7 +237,7 @@ def test_handle_describe_table_with_clustering_columns(self): self.mock_schema_service.describe_table.return_value = table_details # Call the method - result = self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) + result = await self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) # Verify the result self.assertIn('## Table: `mykeyspace.users`', result) @@ -241,19 +253,19 @@ def test_handle_describe_table_with_clustering_columns(self): self.mock_schema_service.describe_table.assert_called_once_with('mykeyspace', 'users') self.mock_context.info.assert_called_once() - def test_handle_describe_table_error(self): + async def test_handle_describe_table_error(self): """Test the _handle_describe_table method with an error.""" # Set up the mock self.mock_schema_service.describe_table.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) + await self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) self.assertIn('Error describing table', str(context.exception)) self.mock_schema_service.describe_table.assert_called_once_with('mykeyspace', 'users') - def test_handle_execute_query(self): + async def test_handle_execute_query(self): """Test the _handle_execute_query method.""" # Set up the mock query_results = { @@ -264,7 +276,7 @@ def test_handle_execute_query(self): self.mock_data_service.execute_read_only_query.return_value = query_results # Call the method - result = self.server._handle_execute_query( + result = await self.server._handle_execute_query( 'mykeyspace', 'SELECT * FROM users', self.mock_context ) @@ -279,14 +291,14 @@ def test_handle_execute_query(self): ) self.mock_context.info.assert_called_once() - def test_handle_execute_query_no_rows(self): - """Test the _handle_execute_query method with no rows.""" + async def test_handle_execute_query_no_rows(self): + """Test the handle_execute_query method with no rows.""" # Set up the mock query_results = {'columns': ['id', 'name'], 'rows': [], 'row_count': 0} self.mock_data_service.execute_read_only_query.return_value = query_results # Call the method - result = self.server._handle_execute_query( + result = await self.server._handle_execute_query( 'mykeyspace', 'SELECT * FROM users WHERE id = 999', self.mock_context ) @@ -300,8 +312,8 @@ def test_handle_execute_query_no_rows(self): ) self.mock_context.info.assert_called_once() - def test_handle_execute_query_many_rows(self): - """Test the _handle_execute_query method with many rows.""" + async def test_handle_execute_query_many_rows(self): + """Test the handle_execute_query method with many rows.""" # Set up the mock rows = [] for i in range(MAX_DISPLAY_ROWS + 5): # More than MAX_DISPLAY_ROWS @@ -311,7 +323,7 @@ def test_handle_execute_query_many_rows(self): self.mock_data_service.execute_read_only_query.return_value = query_results # Call the method - result = self.server._handle_execute_query( + result = await self.server._handle_execute_query( 'mykeyspace', 'SELECT * FROM users', self.mock_context ) @@ -325,11 +337,11 @@ def test_handle_execute_query_many_rows(self): ) self.mock_context.info.assert_called_once() - def test_handle_execute_query_non_select(self): - """Test the _handle_execute_query method with a non-SELECT query.""" + async def test_handle_execute_query_non_select(self): + """Test the handle_execute_query method with a non-SELECT query.""" # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_execute_query( + await self.server._handle_execute_query( 'mykeyspace', "INSERT INTO users (id, name) VALUES (1, 'test')", self.mock_context, @@ -338,11 +350,11 @@ def test_handle_execute_query_non_select(self): self.assertIn('Only SELECT queries are allowed', str(context.exception)) self.mock_data_service.execute_read_only_query.assert_not_called() - def test_handle_execute_query_unsafe_operations(self): - """Test the _handle_execute_query method with unsafe operations.""" + async def test_handle_execute_query_unsafe_operations(self): + """Test the handle_execute_query method with unsafe operations.""" # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_execute_query( + await self.server._handle_execute_query( 'mykeyspace', 'SELECT * FROM users; DROP TABLE users;', self.mock_context, @@ -351,14 +363,14 @@ def test_handle_execute_query_unsafe_operations(self): self.assertIn('potentially unsafe operations', str(context.exception)) self.mock_data_service.execute_read_only_query.assert_not_called() - def test_handle_execute_query_error(self): - """Test the _handle_execute_query method with an error.""" + async def test_handle_execute_query_error(self): + """Test the handle_execute_query method with an error.""" # Set up the mock self.mock_data_service.execute_read_only_query.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_execute_query( + await self.server._handle_execute_query( 'mykeyspace', 'SELECT * FROM users', self.mock_context ) @@ -367,8 +379,8 @@ def test_handle_execute_query_error(self): 'mykeyspace', 'SELECT * FROM users' ) - def test_handle_analyze_query_performance(self): - """Test the _handle_analyze_query_performance method.""" + async def test_handle_analyze_query_performance(self): + """Test the handle_analyze_query_performance method.""" # Set up the mock analysis_result = QueryAnalysisResult( query='SELECT * FROM users WHERE id = 1', @@ -380,7 +392,7 @@ def test_handle_analyze_query_performance(self): self.mock_query_analysis_service.analyze_query.return_value = analysis_result # Call the method - result = self.server._handle_analyze_query_performance( + result = await self.server._handle_analyze_query_performance( 'mykeyspace', 'SELECT * FROM users WHERE id = 1', self.mock_context, @@ -399,8 +411,8 @@ def test_handle_analyze_query_performance(self): ) self.mock_context.info.assert_called_once() - def test_handle_analyze_query_performance_no_recommendations(self): - """Test the _handle_analyze_query_performance method with no recommendations.""" + async def test_handle_analyze_query_performance_no_recommendations(self): + """Test the handle_analyze_query_performance method with no recommendations.""" # Set up the mock analysis_result = QueryAnalysisResult( query='SELECT * FROM users WHERE id = 1', @@ -412,7 +424,7 @@ def test_handle_analyze_query_performance_no_recommendations(self): self.mock_query_analysis_service.analyze_query.return_value = analysis_result # Call the method - result = self.server._handle_analyze_query_performance( + result = await self.server._handle_analyze_query_performance( 'mykeyspace', 'SELECT * FROM users WHERE id = 1', self.mock_context, @@ -430,14 +442,14 @@ def test_handle_analyze_query_performance_no_recommendations(self): ) self.mock_context.info.assert_called_once() - def test_handle_analyze_query_performance_error(self): - """Test the _handle_analyze_query_performance method with an error.""" + async def test_handle_analyze_query_performance_error(self): + """Test the handle_analyze_query_performance method with an error.""" # Set up the mock self.mock_query_analysis_service.analyze_query.side_effect = Exception('Test error') # Call the method and verify it raises an exception with self.assertRaises(Exception) as context: - self.server._handle_analyze_query_performance( + await self.server._handle_analyze_query_performance( 'mykeyspace', 'SELECT * FROM users', self.mock_context ) @@ -471,4 +483,9 @@ def test_get_proxy(mock_app_config, mock_client_class): if __name__ == '__main__': - unittest.main() + import sys + + print('\n' + '='*70) + print('Running Amazon Keyspaces MCP Server Tests') + print('='*70 + '\n') + sys.exit(pytest.main([__file__, '-v', '--tb=short'])) diff --git a/src/amazon-keyspaces-mcp-server/uv.lock b/src/amazon-keyspaces-mcp-server/uv.lock index 1a3a5bd820..7ada75ada3 100644 --- a/src/amazon-keyspaces-mcp-server/uv.lock +++ b/src/amazon-keyspaces-mcp-server/uv.lock @@ -85,7 +85,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "boto3", specifier = ">=1.37.27" }, - { name = "cassandra-driver", specifier = ">=3.25.0" }, + { name = "cassandra-driver", specifier = ">=3.29.2" }, { name = "fastmcp", specifier = ">=2.8.0" }, { name = "loguru", specifier = ">=0.7.0" }, { name = "mcp", specifier = ">=1.11.0" }, From 42265932f5dd316121d14017dc5f26f5af8c4a0a Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 00:03:10 -0800 Subject: [PATCH 05/24] Suppress warning for PytestAssertRewriteWarning --- src/amazon-keyspaces-mcp-server/pyproject.toml | 3 +++ src/amazon-keyspaces-mcp-server/tests/test_server.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/pyproject.toml b/src/amazon-keyspaces-mcp-server/pyproject.toml index db15df3e05..d5f543f655 100644 --- a/src/amazon-keyspaces-mcp-server/pyproject.toml +++ b/src/amazon-keyspaces-mcp-server/pyproject.toml @@ -126,6 +126,9 @@ markers = [ "live: marks tests that make live API calls (deselect with '-m \"not live\"')", "asyncio: marks tests that use asyncio" ] +filterwarnings = [ + "ignore::pytest.PytestAssertRewriteWarning" +] [tool.coverage.report] exclude_also = [ diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index 13f38ed9f4..aecd8a723a 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -13,11 +13,11 @@ # and limitations under the License. """Unit tests for the server module.""" +import pytest + import unittest from unittest.mock import Mock, patch -import pytest - from awslabs.amazon_keyspaces_mcp_server.consts import MAX_DISPLAY_ROWS from awslabs.amazon_keyspaces_mcp_server.models import ( KeyspaceInfo, From 48f636eef9b567bebfe5058df7650fa770ee2fcb Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 00:05:37 -0800 Subject: [PATCH 06/24] Explicitly set the event loop scope for async fixtures --- src/amazon-keyspaces-mcp-server/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/amazon-keyspaces-mcp-server/pyproject.toml b/src/amazon-keyspaces-mcp-server/pyproject.toml index d5f543f655..cab48d02d6 100644 --- a/src/amazon-keyspaces-mcp-server/pyproject.toml +++ b/src/amazon-keyspaces-mcp-server/pyproject.toml @@ -122,6 +122,7 @@ python_classes = "Test*" python_functions = "test_*" testpaths = [ "tests"] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" markers = [ "live: marks tests that make live API calls (deselect with '-m \"not live\"')", "asyncio: marks tests that use asyncio" From 68079b0182f4893e5941f394caf7591005109093 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 00:16:27 -0800 Subject: [PATCH 07/24] Add instruction prompt for LLM --- .../amazon_keyspaces_mcp_server/server.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 543cf35a51..112929bfa8 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -49,9 +49,33 @@ logger.remove() logger.add(sys.stderr, level='INFO') -mcp = FastMCP(name=SERVER_NAME, version=SERVER_VERSION) +mcp = FastMCP( + name=SERVER_NAME, + version=SERVER_VERSION, + instructions=""" +# Amazon Keyspaces MCP Server + +This MCP server enables interaction with Amazon Keyspaces (for Apache Cassandra) and Apache Cassandra databases through natural language. + +## Available Tools + +- **listKeyspaces**: Lists all keyspaces in the database +- **listTables**: Lists tables in a specific keyspace +- **describeKeyspace**: Gets detailed keyspace information including replication strategy +- **describeTable**: Gets table schema including columns, data types, and primary keys +- **executeQuery**: Executes read-only SELECT queries +- **analyzeQueryPerformance**: Analyzes query performance and provides optimization recommendations + +## Usage Guidelines + +1. Start by listing keyspaces to understand the database structure +2. Use describeTable to understand table schemas before querying +3. Only SELECT queries are permitted for data safety +4. Use analyzeQueryPerformance to optimize queries before execution +""", +) -# Global handle to hold the proxy to the specific db client +# Global handle to hold the proxy to the specific database client _PROXY = None From 861ae66dfed12e66abb84b9164da67811dc97389 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 09:31:57 -0800 Subject: [PATCH 08/24] Address pylint docstring warning --- .../awslabs/amazon_keyspaces_mcp_server/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/__init__.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/__init__.py index f053f153f8..2ebfe74800 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/__init__.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/__init__.py @@ -11,7 +11,6 @@ # 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. - -###awslabs.amazon-keyspaces-mcp-server""" +"""Amazon Keyspaces MCP Server package.""" __version__ = '0.0.6' From b33aefc8c82759e90292b762688169bd48de25a5 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 09:34:22 -0800 Subject: [PATCH 09/24] Implement generic exception handling messages --- .../awslabs/amazon_keyspaces_mcp_server/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 112929bfa8..9a59b38c83 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -217,7 +217,7 @@ async def _handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: return formatted_text except Exception as e: logger.error(f'Error listing keyspaces: {str(e)}') - raise SchemaError(f'Error listing keyspaces: {str(e)}') from e + raise SchemaError('Unable to retrieve keyspace information') from e async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None) -> str: """Handle the listTables tool.""" @@ -246,7 +246,7 @@ async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None return formatted_text except Exception as e: logger.error(f'Error listing tables: {str(e)}') - raise SchemaError(f'Error listing tables: {str(e)}') from e + raise SchemaError('Unable to retrieve table information') from e async def _handle_describe_keyspace( self, keyspace: str, ctx: Optional[Context] = None @@ -290,7 +290,7 @@ async def _handle_describe_keyspace( return formatted_text except Exception as e: logger.error(f'Error describing keyspace: {str(e)}') - raise SchemaError(f'Error describing keyspace: {str(e)}') from e + raise SchemaError('Unable to retrieve keyspace details') from e async def _handle_describe_table( self, keyspace: str, table: str, ctx: Optional[Context] = None @@ -359,7 +359,7 @@ async def _handle_describe_table( return formatted_text except Exception as e: logger.error(f'Error describing table: {str(e)}') - raise SchemaError(f'Error describing table: {str(e)}') from e + raise SchemaError('Unable to retrieve table details') from e async def _handle_execute_query( self, keyspace: str, query: str, ctx: Optional[Context] = None @@ -434,7 +434,7 @@ async def _handle_execute_query( raise except Exception as e: logger.error(f'Error executing query: {str(e)}') - raise QueryExecutionError(f'Error executing query: {str(e)}') from e + raise QueryExecutionError('Unable to execute query') from e async def _handle_analyze_query_performance( self, keyspace: str, query: str, ctx: Optional[Context] = None @@ -473,7 +473,7 @@ async def _handle_analyze_query_performance( raise except Exception as e: logger.error(f'Error analyzing query: {str(e)}') - raise QueryExecutionError(f'Error analyzing query: {str(e)}') from e + raise QueryExecutionError('Unable to analyze query') from e def main(): From f1e953d02ef22991e7a27cc192689d443287d1d4 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 09:55:23 -0800 Subject: [PATCH 10/24] Add input data models --- .../amazon_keyspaces_mcp_server/models.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py index 4bd95d7f76..a7a17f9309 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py @@ -13,9 +13,12 @@ # limitations under the License. """Data models for Keyspaces MCP Server.""" +import re from dataclasses import dataclass, field from typing import Any, Dict, List +from pydantic import BaseModel, Field, field_validator + @dataclass class KeyspaceInfo: @@ -126,3 +129,50 @@ class QueryAnalysisResult: is_full_table_scan: bool = False recommendations: List[str] = field(default_factory=list) performance_assessment: str = '' + + +# Pydantic models for input validation +class KeyspaceInput(BaseModel): + """Validated keyspace input.""" + keyspace: str = Field( + ..., + min_length=1, + max_length=48, + pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$', + description='Keyspace name (alphanumeric and underscore only)' + ) + + +class TableInput(BaseModel): + """Validated table input.""" + keyspace: str = Field( + ..., + min_length=1, + max_length=48, + pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' + ) + table: str = Field( + ..., + min_length=1, + max_length=48, + pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' + ) + + +class QueryInput(BaseModel): + """Validated query input.""" + keyspace: str = Field( + ..., + min_length=1, + max_length=48, + pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' + ) + query: str = Field(..., min_length=1, max_length=10000) + + @field_validator('query') + @classmethod + def sanitize_query(cls, v: str) -> str: + """Strip hidden unicode and control characters.""" + v = re.sub(r'[\u200B-\u200D\uFEFF\u0000-\u001F\u007F-\u009F]', '', v) + return v.strip() + From baa8278db5a0e10e705c3a68f5407f0499298555 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 10:02:14 -0800 Subject: [PATCH 11/24] Implement generic exception messages in test_server tests --- src/amazon-keyspaces-mcp-server/tests/test_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index aecd8a723a..cae3d0eba5 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -86,7 +86,7 @@ async def test_handle_list_keyspaces_error(self): with self.assertRaises(Exception) as context: await self.server._handle_list_keyspaces(self.mock_context) - self.assertIn('Error listing keyspaces', str(context.exception)) + self.assertIn('Unable to retrieve keyspace information', str(context.exception)) self.mock_schema_service.list_keyspaces.assert_called_once() async def test_handle_list_tables(self): @@ -129,7 +129,7 @@ async def test_handle_list_tables_error(self): with self.assertRaises(Exception) as context: await self.server._handle_list_tables('mykeyspace', self.mock_context) - self.assertIn('Error listing tables', str(context.exception)) + self.assertIn('Unable to retrieve table information', str(context.exception)) self.mock_schema_service.list_tables.assert_called_once_with('mykeyspace') async def test_handle_describe_keyspace(self): @@ -185,7 +185,7 @@ async def test_handle_describe_keyspace_error(self): with self.assertRaises(Exception) as context: await self.server._handle_describe_keyspace('mykeyspace', self.mock_context) - self.assertIn('Error describing keyspace', str(context.exception)) + self.assertIn('Unable to retrieve keyspace details', str(context.exception)) self.mock_schema_service.describe_keyspace.assert_called_once_with('mykeyspace') async def test_handle_describe_table(self): @@ -262,7 +262,7 @@ async def test_handle_describe_table_error(self): with self.assertRaises(Exception) as context: await self.server._handle_describe_table('mykeyspace', 'users', self.mock_context) - self.assertIn('Error describing table', str(context.exception)) + self.assertIn('Unable to retrieve table details', str(context.exception)) self.mock_schema_service.describe_table.assert_called_once_with('mykeyspace', 'users') async def test_handle_execute_query(self): @@ -374,7 +374,7 @@ async def test_handle_execute_query_error(self): 'mykeyspace', 'SELECT * FROM users', self.mock_context ) - self.assertIn('Error executing query', str(context.exception)) + self.assertIn('Unable to execute query', str(context.exception)) self.mock_data_service.execute_read_only_query.assert_called_once_with( 'mykeyspace', 'SELECT * FROM users' ) @@ -453,7 +453,7 @@ async def test_handle_analyze_query_performance_error(self): 'mykeyspace', 'SELECT * FROM users', self.mock_context ) - self.assertIn('Error analyzing query', str(context.exception)) + self.assertIn('Unable to analyze query', str(context.exception)) self.mock_query_analysis_service.analyze_query.assert_called_once_with( 'mykeyspace', 'SELECT * FROM users' ) From ec2b124a366047b68fea5d886960d9c6356f81b2 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 13:10:08 -0800 Subject: [PATCH 12/24] Set session conneciton to lazily instantiate with async function calls --- .../amazon_keyspaces_mcp_server/client.py | 79 +++++++++++-------- .../amazon_keyspaces_mcp_server/server.py | 62 +++++++++------ .../amazon_keyspaces_mcp_server/services.py | 24 +++--- .../tests/test_server.py | 15 ++-- 4 files changed, 105 insertions(+), 75 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py index 50ede24cc0..fb9662da26 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py @@ -25,7 +25,6 @@ from cassandra.auth import PlainTextAuthProvider from cassandra.cluster import Cluster, Session -from cassandra.io.asyncioreactor import AsyncioConnection from .consts import ( CERT_DIRECTORY, @@ -70,19 +69,23 @@ def __init__(self, database_config: DatabaseConfig): """Initialize the client with the given configuration.""" self.database_config = database_config self.is_keyspaces = database_config.use_keyspaces - - # Initialize session for the configured database type (Keyspaces or Cassandra) - try: - if self.is_keyspaces: - self.session = self._create_keyspaces_session() - logger.info('Connected to Amazon Keyspaces') - else: - self.session = self._create_cassandra_session() - logger.info('Connected to Cassandra cluster') - except Exception as e: - target = 'Amazon Keyspaces' if self.is_keyspaces else 'Cassandra cluster' - logger.error('Failed to connect to %s: %s', target, str(e)) - raise RuntimeError(f'Failed to connect to {target}: {str(e)}') from e + self._session: Optional[Session] = None + + async def get_session(self) -> Session: + """Get or create database session lazily.""" + if self._session is None: + try: + if self.is_keyspaces: + self._session = self._create_keyspaces_session() + logger.info('Connected to Amazon Keyspaces') + else: + self._session = self._create_cassandra_session() + logger.info('Connected to Cassandra cluster') + except Exception as e: + target = 'Amazon Keyspaces' if self.is_keyspaces else 'Cassandra cluster' + logger.error('Failed to connect to %s: %s', target, str(e)) + raise RuntimeError(f'Failed to connect to {target}: {str(e)}') from e + return self._session def _create_cassandra_session(self) -> Session: """Create a session for Apache Cassandra.""" @@ -102,8 +105,6 @@ def _create_cassandra_session(self) -> Session: connect_timeout=int(CONNECTION_TIMEOUT), ) - cluster.connection_class = AsyncioConnection - return cluster.connect() def _create_keyspaces_session(self) -> Session: @@ -144,8 +145,6 @@ def _create_keyspaces_session(self) -> Session: connect_timeout=int(CONNECTION_TIMEOUT), ) - cluster.connection_class = AsyncioConnection - return cluster.connect() def _create_ssl_context_for_keyspaces(self) -> ssl.SSLContext: @@ -171,13 +170,14 @@ def is_using_keyspaces(self) -> bool: """Check if the client is using Amazon Keyspaces.""" return self.is_keyspaces - def list_keyspaces(self) -> List[KeyspaceInfo]: + async def list_keyspaces(self) -> List[KeyspaceInfo]: """List all keyspaces in the database.""" keyspaces = [] try: query = 'SELECT keyspace_name, replication FROM system_schema.keyspaces' - rows = self.session.execute(query) + session = await self.get_session() + rows = session.execute(query) for row in rows: name = row.keyspace_name @@ -199,13 +199,14 @@ def list_keyspaces(self) -> List[KeyspaceInfo]: logger.error('Error listing keyspaces: %s', str(e)) raise RuntimeError(f'Failed to list keyspaces: {str(e)}') from e - def list_tables(self, keyspace_name: str) -> List[TableInfo]: + async def list_tables(self, keyspace_name: str) -> List[TableInfo]: """List all tables in a keyspace.""" tables = [] try: query = 'SELECT table_name FROM system_schema.tables WHERE keyspace_name = %s' - rows = self.session.execute(query, [keyspace_name]) + session = await self.get_session() + rows = session.execute(query, [keyspace_name]) for row in rows: name = row.table_name @@ -218,11 +219,13 @@ def list_tables(self, keyspace_name: str) -> List[TableInfo]: f'Failed to list tables for keyspace {keyspace_name}: {str(e)}' ) from e - def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: + async def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: """Get detailed information about a keyspace.""" try: query = 'SELECT * FROM system_schema.keyspaces WHERE keyspace_name = %s' - row = self.session.execute(query, [keyspace_name]).one() + session = await self.get_session() + + row = session.execute(query, [keyspace_name]).one() if not row: raise RuntimeError(f'Keyspace not found: {keyspace_name}') @@ -245,14 +248,16 @@ def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: logger.error('Error describing keyspace %s: %s', keyspace_name, str(e)) raise RuntimeError(f'Failed to describe keyspace {keyspace_name}: {str(e)}') from e - def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: + async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: """Get detailed information about a table.""" try: query = ( 'SELECT * FROM system_schema.tables WHERE ' 'keyspace_name = %s AND table_name = %s' ) - table_row = self.session.execute(query, [keyspace_name, table_name]).one() + session = await self.get_session() + + table_row = session.execute(query, [keyspace_name, table_name]).one() if not table_row: raise RuntimeError(f'Table not found: {keyspace_name}.{table_name}') @@ -266,7 +271,9 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: query = ( 'SELECT * FROM system_schema.columns WHERE keyspace_name = %s AND table_name = %s' ) - column_rows = self.session.execute(query, [keyspace_name, table_name]) + session = await self.get_session() + + column_rows = session.execute(query, [keyspace_name, table_name]) columns = [] for column_row in column_rows: @@ -284,7 +291,9 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: query = ( 'SELECT * FROM system_schema.indexes WHERE keyspace_name = %s AND table_name = %s' ) - index_rows = self.session.execute(query, [keyspace_name, table_name]) + session = await self.get_session() + + index_rows = session.execute(query, [keyspace_name, table_name]) indexes = [] for index_row in index_rows: @@ -307,7 +316,9 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: 'SELECT custom_properties FROM system_schema_mcs.tables ' 'WHERE keyspace_name = %s AND table_name = %s' ) - capacity_row = self.session.execute( + session = await self.get_session() + + capacity_row = session.execute( query, [keyspace_name, table_name] ).one() @@ -339,7 +350,7 @@ def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: f'Failed to describe table {keyspace_name}.{table_name}: {str(e)}' ) from e - def execute_read_only_query( + async def execute_read_only_query( self, query: str, params: Optional[List[Any]] = None ) -> Dict[str, Any]: """Execute a read-only SELECT query against the database.""" @@ -360,9 +371,13 @@ def execute_read_only_query( # Execute the query if params: - rs = self.session.execute(query, params) + session = await self.get_session() + + rs = session.execute(query, params) else: - rs = self.session.execute(query) + session = await self.get_session() + + rs = session.execute(query) # Process the results rows = [] diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 9a59b38c83..29ef4bab79 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -18,7 +18,6 @@ from fastmcp import Context, FastMCP from loguru import logger -from pydantic import Field from .client import UnifiedCassandraClient from .config import AppConfig @@ -42,6 +41,7 @@ build_query_result_context, build_table_details_context, ) +from .models import KeyspaceInput, QueryInput, TableInput from .services import DataService, QueryAnalysisService, SchemaService @@ -49,6 +49,7 @@ logger.remove() logger.add(sys.stderr, level='INFO') + mcp = FastMCP( name=SERVER_NAME, version=SERVER_VERSION, @@ -79,10 +80,10 @@ _PROXY = None -def get_proxy(): +async def get_proxy(): """Returns a singleton instance of the main Keyspaces MCP server implementation. - The singleton is initialized lazily. + The singleton is initialized lazily when first accessed (ensuring event loop is running). """ global _PROXY # pylint: disable=global-statement if _PROXY is None: @@ -110,7 +111,8 @@ async def list_keyspaces( ctx: Optional[Context] = None, ) -> str: """Lists all keyspaces in the Cassandra/Keyspaces database.""" - return await get_proxy()._handle_list_keyspaces(ctx) # pylint: disable=protected-access + proxy = await get_proxy() + return await proxy._handle_list_keyspaces(ctx) # pylint: disable=protected-access @mcp.tool( @@ -118,11 +120,12 @@ async def list_keyspaces( description='Lists all tables in a specified keyspace - args: keyspace', ) async def list_tables( - keyspace: str = Field(..., description='The keyspace to list tables from.'), + input: KeyspaceInput, ctx: Optional[Context] = None, ) -> str: """Lists all tables in a specified keyspace.""" - return await get_proxy()._handle_list_tables(keyspace, ctx) # pylint: disable=protected-access + proxy = await get_proxy() + return await proxy._handle_list_tables(input.keyspace, ctx) # pylint: disable=protected-access @mcp.tool( @@ -130,11 +133,12 @@ async def list_tables( description='Gets detailed information about a keyspace - args: keyspace', ) async def describe_keyspace( - keyspace: str = Field(..., description='The keyspace to retrieve metadata for.'), + input: KeyspaceInput, ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a keyspace.""" - return await get_proxy()._handle_describe_keyspace(keyspace, ctx) # pylint: disable=protected-access + proxy = await get_proxy() + return await proxy._handle_describe_keyspace(input.keyspace, ctx) # pylint: disable=protected-access @mcp.tool( @@ -142,12 +146,12 @@ async def describe_keyspace( description='Gets detailed information about a table - args: keyspace, table', ) async def describe_table( - keyspace: str = Field(..., description='The keyspace containing the table'), - table: str = Field(..., description='The name of the table to describe'), + input: TableInput, ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a table.""" - return await get_proxy()._handle_describe_table(keyspace, table, ctx) # pylint: disable=protected-access + proxy = await get_proxy() + return await proxy._handle_describe_table(input.keyspace, input.table, ctx) # pylint: disable=protected-access @mcp.tool( @@ -155,12 +159,12 @@ async def describe_table( description='Executes a read-only SELECT query against the database - args: keyspace, query', ) async def execute_query( - keyspace: str = Field(..., description='The keyspace to execute the query against'), - query: str = Field(..., description='The CQL SELECT query to execute'), + input: QueryInput, ctx: Optional[Context] = None, ) -> str: """Executes a read-only (SELECT) query against the database.""" - return await get_proxy()._handle_execute_query(keyspace, query, ctx) # pylint: disable=protected-access + proxy = await get_proxy() + return await proxy._handle_execute_query(input.keyspace, input.query, ctx) # pylint: disable=protected-access @mcp.tool( @@ -168,14 +172,13 @@ async def execute_query( description='Analyzes the performance characteristics of a CQL query - args: keyspace, query', ) async def analyze_query_performance( - keyspace: str = Field(..., description='The keyspace to analyze the query against'), - query: str = Field(..., description='The CQL query to analyze for performance'), + input: QueryInput, ctx: Optional[Context] = None, ) -> str: """Analyzes the performance characteristics of a CQL query.""" - proxy = get_proxy() + proxy = await get_proxy() return await proxy._handle_analyze_query_performance( # pylint: disable=protected-access - keyspace, query, ctx + input.keyspace, input.query, ctx ) @@ -196,7 +199,7 @@ def __init__( async def _handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: """Handle the listKeyspaces tool.""" try: - keyspaces = self.schema_service.list_keyspaces() + keyspaces = await self.schema_service.list_keyspaces() # Format keyspace names as a markdown list for better display keyspace_names = [k.name for k in keyspaces] @@ -225,7 +228,7 @@ async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None if not keyspace: raise ValidationError('Keyspace name is required') - tables = self.schema_service.list_tables(keyspace) + tables = await self.schema_service.list_tables(keyspace) # Format table names as a markdown list for better display table_names = [t.name for t in tables] @@ -256,7 +259,7 @@ async def _handle_describe_keyspace( if not keyspace: raise ValidationError('Keyspace name is required') - keyspace_details = self.schema_service.describe_keyspace(keyspace) + keyspace_details = await self.schema_service.describe_keyspace(keyspace) # Format keyspace details as markdown formatted_text = f'## Keyspace: `{keyspace}`\n\n' @@ -303,7 +306,7 @@ async def _handle_describe_table( if not table: raise ValidationError('Table name is required') - table_details = self.schema_service.describe_table(keyspace, table) + table_details = await self.schema_service.describe_table(keyspace, table) # Format table details as markdown formatted_text = f'## Table: `{keyspace}.{table}`\n\n' @@ -382,7 +385,7 @@ async def _handle_execute_query( raise QuerySecurityError('Query contains potentially unsafe operations') # Execute the query using the DataService - query_results = self.data_service.execute_read_only_query(keyspace, query) + query_results = await self.data_service.execute_read_only_query(keyspace, query) # Format the results for display formatted_text = '## Query Results\n\n' @@ -447,7 +450,7 @@ async def _handle_analyze_query_performance( if not query: raise ValidationError('Query is required') - analysis_result = self.query_analysis_service.analyze_query(keyspace, query) + analysis_result = await self.query_analysis_service.analyze_query(keyspace, query) # Build a user-friendly response formatted_text = '## Query Analysis Results\n\n' @@ -478,6 +481,17 @@ async def _handle_analyze_query_performance( def main(): """Run the MCP server.""" + import asyncio + + # Validate connection before starting server + try: + proxy = asyncio.run(get_proxy()) + asyncio.run(proxy.schema_service.cassandra_client.get_session()) + logger.success('Successfully validated database connection') + except Exception as e: + logger.error(f'Failed to connect to database: {e}') + sys.exit(1) + mcp.run() diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py index f5265b784b..fa85ef4dae 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py @@ -35,7 +35,7 @@ def __init__(self, cassandra_client: UnifiedCassandraClient): cassandra_client.is_using_keyspaces(), ) - def execute_read_only_query(self, keyspace_name: str, query: str) -> Dict[str, Any]: + async def execute_read_only_query(self, keyspace_name: str, query: str) -> Dict[str, Any]: """Execute a read-only SELECT query against the database.""" logger.info('Executing read-only query on keyspace %s: %s', keyspace_name, query) @@ -86,25 +86,25 @@ def __init__(self, cassandra_client: UnifiedCassandraClient): cassandra_client.is_using_keyspaces(), ) - def list_keyspaces(self) -> List[KeyspaceInfo]: + async def list_keyspaces(self) -> List[KeyspaceInfo]: """List all keyspaces in the database.""" logger.info('Listing keyspaces') - return self.cassandra_client.list_keyspaces() + return await self.cassandra_client.list_keyspaces() - def list_tables(self, keyspace_name: str) -> List[TableInfo]: + async def list_tables(self, keyspace_name: str) -> List[TableInfo]: """List all tables in a keyspace.""" logger.info('Listing tables for keyspace: %s', keyspace_name) - return self.cassandra_client.list_tables(keyspace_name) + return await self.cassandra_client.list_tables(keyspace_name) - def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: + async def describe_keyspace(self, keyspace_name: str) -> Dict[str, Any]: """Get detailed information about a keyspace.""" logger.info('Describing keyspace: %s', keyspace_name) - return self.cassandra_client.describe_keyspace(keyspace_name) + return await self.cassandra_client.describe_keyspace(keyspace_name) - def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: + async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, Any]: """Get detailed information about a table.""" logger.info('Describing table: %s.%s', keyspace_name, table_name) - return self.cassandra_client.describe_table(keyspace_name, table_name) + return await self.cassandra_client.describe_table(keyspace_name, table_name) class QueryAnalysisService: @@ -116,7 +116,7 @@ def __init__(self, cassandra_client: UnifiedCassandraClient, schema_service: Sch self.schema_service = schema_service logger.info('QueryAnalysisService initialized') - def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: + async def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: """Analyze a CQL query for performance characteristics.""" logger.info('Analyzing query for keyspace %s: %s', keyspace_name, query) @@ -139,7 +139,7 @@ def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: return result # Get table schema information - tables = self.schema_service.list_tables(keyspace_name) + tables = await self.schema_service.list_tables(keyspace_name) table_info = next((t for t in tables if t.name.lower() == table_name.lower()), None) if not table_info: @@ -150,7 +150,7 @@ def analyze_query(self, keyspace_name: str, query: str) -> QueryAnalysisResult: return result # Get table details - table_details = self.schema_service.describe_table(keyspace_name, table_name) + table_details = await self.schema_service.describe_table(keyspace_name, table_name) # Extract WHERE conditions where_conditions = self._extract_where_conditions(normalized_query) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index cae3d0eba5..750a3fc02b 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -16,7 +16,7 @@ import pytest import unittest -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from awslabs.amazon_keyspaces_mcp_server.consts import MAX_DISPLAY_ROWS from awslabs.amazon_keyspaces_mcp_server.models import ( @@ -37,9 +37,9 @@ class TestKeyspacesMcpStdioServer(unittest.IsolatedAsyncioTestCase): def setUp(self): """Set up test fixtures.""" - self.mock_data_service = Mock() - self.mock_query_analysis_service = Mock() - self.mock_schema_service = Mock() + self.mock_data_service = AsyncMock() + self.mock_query_analysis_service = AsyncMock() + self.mock_schema_service = AsyncMock() self.server = KeyspacesMcpStdioServer( self.mock_data_service, self.mock_query_analysis_service, self.mock_schema_service ) @@ -461,7 +461,8 @@ async def test_handle_analyze_query_performance_error(self): @patch('awslabs.amazon_keyspaces_mcp_server.server.UnifiedCassandraClient') @patch('awslabs.amazon_keyspaces_mcp_server.server.AppConfig') -def test_get_proxy(mock_app_config, mock_client_class): +@pytest.mark.asyncio +async def test_get_proxy(mock_app_config, mock_client_class): """Test the get_proxy function.""" # Set up the mocks mock_app_config_instance = Mock() @@ -471,10 +472,10 @@ def test_get_proxy(mock_app_config, mock_client_class): mock_client_class.return_value = mock_client_instance # Call the function - proxy = get_proxy() + proxy = await get_proxy() # Call it again to test singleton behavior - proxy2 = get_proxy() + proxy2 = await get_proxy() # Verify the results assert proxy is proxy2 # Should return the same instance From 4bea05d6b5c32f7f31fcaf84000f9bcc25d60f2d Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 13:10:34 -0800 Subject: [PATCH 13/24] Update LLM context limitations --- .../amazon_keyspaces_mcp_server/llm_context.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py index 83d8c2dc4a..935f151e0c 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py @@ -34,7 +34,7 @@ def build_list_keyspaces_context(_keyspaces: List[KeyspaceInfo]) -> str: ), "limitations": ( "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " - "Unsupported features include logged batches, materialized views, " + "Unsupported features include materialized views, " "indexes, aggregate functions like COUNT and SUM, prepared statements " "for DDL operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, " "the inequality operator for user-defined types, or the IN keyword in " @@ -83,6 +83,16 @@ def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> "entity they represent." ), } + + # Add guidance for empty results + if not _tables: + tables_guidance["empty_result_interpretation"] = ( + "When no tables are found, this could mean either: (1) the keyspace exists " + "but contains no tables, or (2) the keyspace does not exist. If you suspect " + "the keyspace might not exist, use the listKeyspaces tool to verify the " + "keyspace name is correct before concluding it's empty." + ) + context['tables_guidance'] = tables_guidance return dict_to_markdown(context) @@ -281,7 +291,7 @@ def build_amazon_keyspaces_knowledge() -> Dict[str, str]: ), "differences_from_cassandra": ( "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " - "Unsupported features include logged batches, materialized views, indexes, " + "Unsupported features include materialized views, indexes, " "aggregate functions like COUNT and SUM, prepared statements for DDL " "operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, the " "inequality operator for user-defined types, or the IN keyword in INSERT " From 15fc5f3851a8beb273d1b5a7e14ac4a3d2367ada Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 16:45:31 -0800 Subject: [PATCH 14/24] Increase test coverage --- .../amazon_keyspaces_mcp_server/client.py | 8 +- .../amazon_keyspaces_mcp_server/services.py | 2 +- .../tests/__init__.py | 11 +- .../tests/test_client.py | 131 ++++++++++------ .../tests/test_init.py | 29 ++-- .../tests/test_main.py | 27 ++-- .../tests/test_models.py | 35 +++++ .../tests/test_query_analysis_service.py | 143 +++++++++--------- .../tests/test_server.py | 82 +++++++++- .../tests/test_services.py | 107 +++++-------- 10 files changed, 346 insertions(+), 229 deletions(-) create mode 100644 src/amazon-keyspaces-mcp-server/tests/test_models.py diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py index fb9662da26..36ba086e6a 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py @@ -456,8 +456,8 @@ def _build_service_characteristics(self) -> Dict[str, Any]: def close(self) -> None: """Close the session.""" - if hasattr(self, 'session') and self.session: - if self.session.cluster: - self.session.cluster.shutdown() - self.session.shutdown() + if self._session: + if self._session.cluster: + self._session.cluster.shutdown() + self._session.shutdown() logger.info('Closed session') diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py index fa85ef4dae..c53f6f3ab8 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py @@ -72,7 +72,7 @@ async def execute_read_only_query(self, keyspace_name: str, query: str) -> Dict[ + query[table_name_start:] ) - return self.cassandra_client.execute_read_only_query(full_query) + return await self.cassandra_client.execute_read_only_query(full_query) class SchemaService: diff --git a/src/amazon-keyspaces-mcp-server/tests/__init__.py b/src/amazon-keyspaces-mcp-server/tests/__init__.py index 7b3c4c6159..b5362f8a28 100644 --- a/src/amazon-keyspaces-mcp-server/tests/__init__.py +++ b/src/amazon-keyspaces-mcp-server/tests/__init__.py @@ -1,12 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """ Test package for keyspaces-mcp. diff --git a/src/amazon-keyspaces-mcp-server/tests/test_client.py b/src/amazon-keyspaces-mcp-server/tests/test_client.py index 6c209ebff1..f381acf158 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_client.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_client.py @@ -1,26 +1,36 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Unit tests for the UnifiedCassandraClient class.""" import ssl import unittest +from unittest.mock import Mock, patch + +from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster, Session + from awslabs.amazon_keyspaces_mcp_server.client import UnifiedCassandraClient from awslabs.amazon_keyspaces_mcp_server.config import DatabaseConfig +from awslabs.amazon_keyspaces_mcp_server.consts import ( + CASSANDRA_DEFAULT_PORT, + KEYSPACES_DEFAULT_PORT, +) from awslabs.amazon_keyspaces_mcp_server.models import TableInfo -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, Session -from unittest.mock import Mock, patch -class TestUnifiedCassandraClient(unittest.TestCase): +# pylint: disable=no-member +class TestUnifiedCassandraClient(unittest.IsolatedAsyncioTestCase): """Tests for the UnifiedCassandraClient class.""" def setUp(self): @@ -55,22 +65,23 @@ def setUp(self): self.mock_cluster.connect.return_value = self.mock_session @patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') - def test_create_cassandra_session(self, mock_cluster_class): + async def test_create_cassandra_session(self, mock_cluster_class): """Test creating a session for Apache Cassandra.""" # Set up the mock mock_cluster_instance = mock_cluster_class.return_value mock_cluster_instance.connect.return_value = self.mock_session - # Create the client + # Create the client and get session client = UnifiedCassandraClient(self.cassandra_config) + await client.get_session() # Verify Cluster was called with the correct arguments mock_cluster_class.assert_called_once() - args, kwargs = mock_cluster_class.call_args + _, kwargs = mock_cluster_class.call_args # Check that contact points and port are correct self.assertEqual(kwargs['contact_points'], ['127.0.0.1']) - self.assertEqual(kwargs['port'], 9042) + self.assertEqual(kwargs['port'], CASSANDRA_DEFAULT_PORT) # Check that auth provider is correctly configured self.assertIsInstance(kwargs['auth_provider'], PlainTextAuthProvider) @@ -79,7 +90,7 @@ def test_create_cassandra_session(self, mock_cluster_class): mock_cluster_instance.connect.assert_called_once() # Verify the session is set correctly - self.assertEqual(client.session, self.mock_session) + self.assertEqual(client._session, self.mock_session) # pylint: disable=protected-access # Verify is_keyspaces is set correctly self.assertFalse(client.is_keyspaces) @@ -89,7 +100,7 @@ def test_create_cassandra_session(self, mock_cluster_class): @patch('awslabs.amazon_keyspaces_mcp_server.client.os.path.join') @patch('awslabs.amazon_keyspaces_mcp_server.client.os.path.dirname') @patch('awslabs.amazon_keyspaces_mcp_server.client.HAS_SSL_OPTIONS', True) - def test_create_keyspaces_session_with_ssl_options( + async def test_create_keyspaces_session_with_ssl_options( self, mock_dirname, mock_join, mock_ssl, mock_cluster_class ): """Test creating a session for Amazon Keyspaces with SSLOptions.""" @@ -103,8 +114,9 @@ def test_create_keyspaces_session_with_ssl_options( mock_dirname.return_value = '/mock/path' mock_join.return_value = '/mock/path/certs/sf-class2-root.crt' - # Create the client + # Create the client and get session client = UnifiedCassandraClient(self.keyspaces_config) + await client.get_session() # Verify ssl.create_default_context was called mock_ssl.create_default_context.assert_called_once() @@ -119,11 +131,11 @@ def test_create_keyspaces_session_with_ssl_options( # Verify Cluster was called with the correct arguments mock_cluster_class.assert_called_once() - args, kwargs = mock_cluster_class.call_args + _, kwargs = mock_cluster_class.call_args # Check that contact points and port are correct self.assertEqual(kwargs['contact_points'], ['cassandra.us-west-2.amazonaws.com']) - self.assertEqual(kwargs['port'], 9142) # Default Keyspaces port + self.assertEqual(kwargs['port'], KEYSPACES_DEFAULT_PORT) # Default Keyspaces port # Check that auth provider is correctly configured self.assertIsInstance(kwargs['auth_provider'], PlainTextAuthProvider) @@ -135,7 +147,7 @@ def test_create_keyspaces_session_with_ssl_options( mock_cluster_instance.connect.assert_called_once() # Verify the session is set correctly - self.assertEqual(client.session, self.mock_session) + self.assertEqual(client._session, self.mock_session) # pylint: disable=protected-access # Verify is_keyspaces is set correctly self.assertTrue(client.is_keyspaces) @@ -145,7 +157,7 @@ def test_create_keyspaces_session_with_ssl_options( @patch('awslabs.amazon_keyspaces_mcp_server.client.os.path.join') @patch('awslabs.amazon_keyspaces_mcp_server.client.os.path.dirname') @patch('awslabs.amazon_keyspaces_mcp_server.client.HAS_SSL_OPTIONS', False) - def test_create_keyspaces_session_without_ssl_options( + async def test_create_keyspaces_session_without_ssl_options( self, mock_dirname, mock_join, mock_ssl, mock_cluster_class ): """Test creating a session for Amazon Keyspaces without SSLOptions.""" @@ -159,8 +171,9 @@ def test_create_keyspaces_session_without_ssl_options( mock_dirname.return_value = '/mock/path' mock_join.return_value = '/mock/path/certs/sf-class2-root.crt' - # Create the client + # Create the client and get session client = UnifiedCassandraClient(self.keyspaces_config) + await client.get_session() # Verify ssl.create_default_context was called mock_ssl.create_default_context.assert_called_once() @@ -175,7 +188,7 @@ def test_create_keyspaces_session_without_ssl_options( # Verify Cluster was called with the correct arguments mock_cluster_class.assert_called_once() - args, kwargs = mock_cluster_class.call_args + _, kwargs = mock_cluster_class.call_args # Check that contact points and port are correct self.assertEqual(kwargs['contact_points'], ['cassandra.us-west-2.amazonaws.com']) @@ -191,14 +204,14 @@ def test_create_keyspaces_session_without_ssl_options( mock_cluster_instance.connect.assert_called_once() # Verify the session is set correctly - self.assertEqual(client.session, self.mock_session) + self.assertEqual(client._session, self.mock_session) # pylint: disable=protected-access # Verify is_keyspaces is set correctly self.assertTrue(client.is_keyspaces) @patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') @patch('awslabs.amazon_keyspaces_mcp_server.client.ssl') - def test_ssl_context_load_error(self, mock_ssl, mock_cluster_class): + async def test_ssl_context_load_error(self, mock_ssl, mock_cluster_class): """Test handling of SSL certificate loading errors.""" # Set up the mocks mock_cluster_instance = mock_cluster_class.return_value @@ -206,18 +219,20 @@ def test_ssl_context_load_error(self, mock_ssl, mock_cluster_class): mock_ssl_context = Mock(spec=ssl.SSLContext) mock_ssl.create_default_context.return_value = mock_ssl_context + mock_ssl.SSLError = ssl.SSLError # Make load_verify_locations raise an exception - mock_ssl_context.load_verify_locations.side_effect = Exception('Certificate not found') + mock_ssl_context.load_verify_locations.side_effect = FileNotFoundError('Certificate not found') - # Create the client + # Create the client and get session client = UnifiedCassandraClient(self.keyspaces_config) + await client.get_session() # Verify load_default_certs was called as a fallback mock_ssl_context.load_default_certs.assert_called_once() # Verify the client was still created successfully - self.assertEqual(client.session, self.mock_session) + self.assertEqual(client._session, self.mock_session) # pylint: disable=protected-access def test_is_using_keyspaces(self): """Test the is_using_keyspaces method.""" @@ -230,7 +245,7 @@ def test_is_using_keyspaces(self): self.assertFalse(cassandra_client.is_using_keyspaces()) self.assertTrue(keyspaces_client.is_using_keyspaces()) - def test_list_keyspaces(self): + async def test_list_keyspaces(self): """Test listing keyspaces.""" mock_row1 = Mock() mock_row1.keyspace_name = 'system' @@ -250,7 +265,7 @@ def test_list_keyspaces(self): client = UnifiedCassandraClient(self.cassandra_config) # Call the method - keyspaces = client.list_keyspaces() + keyspaces = await client.list_keyspaces() # Verify the session.execute was called with the correct query self.mock_session.execute.assert_called_once_with( @@ -265,7 +280,7 @@ def test_list_keyspaces(self): self.assertEqual(keyspaces[1].name, 'mykeyspace') self.assertEqual(keyspaces[1].replication_strategy, 'NetworkTopologyStrategy') - def test_describe_keyspace(self): + async def test_describe_keyspace(self): """Test describing a keyspace.""" # Set up the mock session mock_row = Mock() @@ -295,7 +310,7 @@ def test_describe_keyspace(self): ) # Call the method - keyspace_details = client.describe_keyspace('mykeyspace') + keyspace_details = await client.describe_keyspace('mykeyspace') # Check if execute was called at all self.assertTrue(self.mock_session.execute.called, 'session.execute was not called') @@ -312,7 +327,7 @@ def test_describe_keyspace(self): self.assertTrue(keyspace_details['durable_writes']) self.assertEqual(len(keyspace_details['tables']), 2) - def test_list_tables(self): + async def test_list_tables(self): """Test listing tables in a keyspace.""" # Set up the mock session mock_row1 = Mock() @@ -332,7 +347,7 @@ def test_list_tables(self): client = UnifiedCassandraClient(self.cassandra_config) # Call the method - tables = client.list_tables('mykeyspace') + tables = await client.list_tables('mykeyspace') # Verify the session.execute was called with the correct query self.mock_session.execute.assert_called_once_with( @@ -347,7 +362,7 @@ def test_list_tables(self): self.assertEqual(tables[1].name, 'products') self.assertEqual(tables[1].keyspace, 'mykeyspace') - def test_describe_keyspace_not_found(self): + async def test_describe_keyspace_not_found(self): """Test describing a keyspace that doesn't exist.""" # Set up the mock session to return None self.mock_session.execute.return_value = Mock() @@ -363,11 +378,11 @@ def test_describe_keyspace_not_found(self): # Call the method and verify it raises an exception with self.assertRaises(RuntimeError) as context: - client.describe_keyspace('nonexistent') + await client.describe_keyspace('nonexistent') self.assertIn('Keyspace not found', str(context.exception)) - def test_describe_table(self): + async def test_describe_table(self): """Test describing a table.""" # Set up the mock session for table query mock_table_row = Mock() @@ -395,7 +410,7 @@ def test_describe_table(self): mock_index_row.options = {'target': 'name'} # Configure the execute method to return different results based on the query - def mock_execute(query, params=None): + def mock_execute(query, _params=None): if 'tables' in query: result = Mock() result.one.return_value = mock_table_row @@ -416,7 +431,7 @@ def mock_execute(query, params=None): client = UnifiedCassandraClient(self.cassandra_config) # Call the method - table_details = client.describe_table('mykeyspace', 'users') + table_details = await client.describe_table('mykeyspace', 'users') # Verify the result self.assertEqual(table_details['name'], 'users') @@ -432,7 +447,7 @@ def mock_execute(query, params=None): self.assertEqual(table_details['indexes'][0]['name'], 'name_idx') self.assertEqual(table_details['indexes'][0]['options']['target'], 'name') - def test_describe_table_not_found(self): + async def test_describe_table_not_found(self): """Test describing a table that doesn't exist.""" with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: # Configure the mock cluster to return our mock session @@ -445,11 +460,11 @@ def test_describe_table_not_found(self): # Call the method and verify it raises an exception with self.assertRaises(RuntimeError) as context: - client.describe_table('mykeyspace', 'nonexistent') + await client.describe_table('mykeyspace', 'nonexistent') self.assertIn('Table not found', str(context.exception)) - def test_execute_read_only_query(self): + async def test_execute_read_only_query(self): """Test executing a read-only query.""" # Set up the mock session mock_column_names = ['id', 'name', 'value'] @@ -482,7 +497,7 @@ def test_execute_read_only_query(self): client = UnifiedCassandraClient(self.cassandra_config) # Call the method - result = client.execute_read_only_query('SELECT * FROM users WHERE id = 1') + result = await client.execute_read_only_query('SELECT * FROM users WHERE id = 1') # Verify the session.execute was called with the correct query self.mock_session.execute.assert_called_once_with( @@ -498,7 +513,7 @@ def test_execute_read_only_query(self): self.assertEqual(result['row_count'], 1) self.assertEqual(result['execution_info']['queried_host'], '127.0.0.1') - def test_execute_read_only_query_with_params(self): + async def test_execute_read_only_query_with_params(self): """Test executing a read-only query with parameters.""" # Set up the mock session mock_column_names = ['id', 'name'] @@ -525,7 +540,9 @@ def test_execute_read_only_query_with_params(self): # Call the method with parameters params = [1] - result = client.execute_read_only_query('SELECT * FROM users WHERE id = %s', params) + result = await client.execute_read_only_query( + 'SELECT * FROM users WHERE id = %s', params + ) # Verify the session.execute was called with the correct query and parameters self.mock_session.execute.assert_called_once_with( @@ -539,7 +556,7 @@ def test_execute_read_only_query_with_params(self): self.assertEqual(result['rows'][0]['name'], 'test') self.assertEqual(result['row_count'], 1) - def test_execute_read_only_query_non_select(self): + async def test_execute_read_only_query_non_select(self): """Test executing a non-SELECT query.""" # Create the client with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster'): @@ -547,23 +564,26 @@ def test_execute_read_only_query_non_select(self): # Call the method with a non-SELECT query and verify it raises an exception with self.assertRaises(ValueError) as context: - client.execute_read_only_query("INSERT INTO users (id, name) VALUES (1, 'test')") + await client.execute_read_only_query( + "INSERT INTO users (id, name) VALUES (1, 'test')" + ) self.assertIn('Only SELECT queries are allowed', str(context.exception)) - def test_execute_read_only_query_unsafe_operations(self): + async def test_execute_read_only_query_unsafe_operations(self): """Test executing a query with unsafe operations.""" # Create the client with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster'): client = UnifiedCassandraClient(self.cassandra_config) - # Call the method with a query containing unsafe operations and verify it raises an exception + # Call the method with a query containing unsafe operations + # and verify it raises an exception with self.assertRaises(ValueError) as context: - client.execute_read_only_query('SELECT * FROM users; DROP TABLE users;') + await client.execute_read_only_query('SELECT * FROM users; DROP TABLE users;') self.assertIn('potentially unsafe operations', str(context.exception)) - def test_close(self): + async def test_close(self): """Test closing the client.""" # Create the client with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: @@ -572,6 +592,7 @@ def test_close(self): mock_cluster_instance.connect.return_value = self.mock_session client = UnifiedCassandraClient(self.cassandra_config) + await client.get_session() # Call the close method client.close() @@ -580,6 +601,18 @@ def test_close(self): self.mock_session.cluster.shutdown.assert_called_once() self.mock_session.shutdown.assert_called_once() + async def test_get_session_connection_failure(self): + """Test get_session when connection fails.""" + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_class.return_value.connect.side_effect = Exception('Connection failed') + + client = UnifiedCassandraClient(self.cassandra_config) + + with self.assertRaises(RuntimeError) as context: + await client.get_session() + + self.assertIn('Failed to connect to Cassandra cluster', str(context.exception)) + if __name__ == '__main__': unittest.main() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_init.py b/src/amazon-keyspaces-mcp-server/tests/test_init.py index 5a5df39c00..6ec29714a9 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_init.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_init.py @@ -1,12 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Tests for the awslabs.amazon-keyspaces-mcp-server package.""" @@ -19,31 +22,23 @@ class TestInit: def test_version(self): """Test that __version__ is defined and follows semantic versioning.""" - # Import the module + # pylint: disable=import-outside-toplevel import awslabs.amazon_keyspaces_mcp_server - # Check that __version__ is defined assert hasattr(awslabs.amazon_keyspaces_mcp_server, '__version__') - - # Check that __version__ is a string assert isinstance(awslabs.amazon_keyspaces_mcp_server.__version__, str) - # Check that __version__ follows semantic versioning (major.minor.patch) version_pattern = r'^\d+\.\d+\.\d+$' - assert re.match(version_pattern, awslabs.amazon_keyspaces_mcp_server.__version__), ( - f"Version '{awslabs.amazon_keyspaces_mcp_server.__version__}' does not follow semantic versioning" + version = awslabs.amazon_keyspaces_mcp_server.__version__ + assert re.match(version_pattern, version), ( + f"Version '{version}' does not follow semantic versioning" ) def test_module_reload(self): """Test that the module can be reloaded.""" - # Import the module + # pylint: disable=import-outside-toplevel import awslabs.amazon_keyspaces_mcp_server - # Store the original version original_version = awslabs.amazon_keyspaces_mcp_server.__version__ - - # Reload the module importlib.reload(awslabs.amazon_keyspaces_mcp_server) - - # Check that the version is still the same assert awslabs.amazon_keyspaces_mcp_server.__version__ == original_version diff --git a/src/amazon-keyspaces-mcp-server/tests/test_main.py b/src/amazon-keyspaces-mcp-server/tests/test_main.py index 402922f60a..2f2aaf0772 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_main.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_main.py @@ -1,18 +1,23 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Tests for the main function in server.py.""" -from awslabs.amazon_keyspaces_mcp_server.server import main +import inspect from unittest.mock import patch +from awslabs.amazon_keyspaces_mcp_server.server import main + class TestMain: """Tests for the main function.""" @@ -27,26 +32,16 @@ def test_main_default(self, mock_run): 2. The mcp.run method is called once 3. No transport parameter is passed to mcp.run """ - # Call the main function main() - - # Check that mcp.run was called with the correct arguments mock_run.assert_called_once() assert mock_run.call_args[1].get('transport') is None def test_module_execution(self): """Test the module execution when run as __main__.""" - # This test directly executes the code in the if __name__ == '__main__': block - # to ensure coverage of that line - - # Get the source code of the module - import inspect + # pylint: disable=import-outside-toplevel from awslabs.amazon_keyspaces_mcp_server import server - # Get the source code source = inspect.getsource(server) - - # Check that the module has the if __name__ == '__main__': block assert "if __name__ == '__main__':" in source assert 'main()' in source diff --git a/src/amazon-keyspaces-mcp-server/tests/test_models.py b/src/amazon-keyspaces-mcp-server/tests/test_models.py new file mode 100644 index 0000000000..0e01439279 --- /dev/null +++ b/src/amazon-keyspaces-mcp-server/tests/test_models.py @@ -0,0 +1,35 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# 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. + +"""Tests for models module.""" + +import unittest + +from awslabs.amazon_keyspaces_mcp_server.models import QueryInput + + +class TestModels(unittest.TestCase): + """Test cases for models.""" + + def test_sanitize_query_with_hidden_characters(self): + """Test query sanitization removes hidden unicode characters.""" + input_data = QueryInput( + keyspace='test', + query='SELECT\u200B * FROM\uFEFF users\u0000' + ) + self.assertEqual(input_data.query, 'SELECT * FROM users') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py b/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py index f6082ca61d..856934eaf7 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py @@ -1,22 +1,30 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Unit tests for the QueryAnalysisService class.""" import unittest +from unittest.mock import AsyncMock, Mock, PropertyMock + from awslabs.amazon_keyspaces_mcp_server.models import QueryAnalysisResult -from awslabs.amazon_keyspaces_mcp_server.services import QueryAnalysisService, SchemaService -from unittest.mock import Mock, PropertyMock +from awslabs.amazon_keyspaces_mcp_server.services import ( + QueryAnalysisService, + SchemaService, +) -class TestQueryAnalysisService(unittest.TestCase): +# pylint: disable=protected-access +class TestQueryAnalysisService(unittest.IsolatedAsyncioTestCase): """Tests for the QueryAnalysisService class.""" def setUp(self): @@ -217,29 +225,28 @@ def test_generate_performance_assessment_with_secondary_index(self): self.assertIn('SECONDARY INDEX USAGE', result.performance_assessment) self.assertIn('Monitor the performance', ' '.join(result.recommendations)) - def test_analyze_query_integration(self): + async def test_analyze_query_integration(self): """Test the analyze_query method with a complete integration test.""" - # Mock the schema service responses table_info_mock = Mock() type(table_info_mock).name = PropertyMock(return_value='users') - self.mock_schema_service.list_tables.return_value = [table_info_mock] - self.mock_schema_service.describe_table.return_value = { - 'columns': [ - {'name': 'id', 'is_partition_key': True}, - {'name': 'name', 'is_partition_key': False}, - {'name': 'created_at', 'is_clustering_column': True}, - ], - 'partition_key': ['id'], - 'clustering_columns': ['created_at'], - 'indexes': [], - } + self.mock_schema_service.list_tables = AsyncMock(return_value=[table_info_mock]) + self.mock_schema_service.describe_table = AsyncMock( + return_value={ + 'columns': [ + {'name': 'id', 'is_partition_key': True}, + {'name': 'name', 'is_partition_key': False}, + {'name': 'created_at', 'is_clustering_column': True}, + ], + 'partition_key': ['id'], + 'clustering_columns': ['created_at'], + 'indexes': [], + } + ) - # Call the analyze_query method - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', "SELECT * FROM users WHERE id = 1 AND name = 'test'" ) - # Verify the result self.assertEqual(result.table_name, 'users') self.assertTrue(result.uses_partition_key) self.assertFalse(result.uses_clustering_columns) @@ -248,99 +255,97 @@ def test_analyze_query_integration(self): self.assertFalse(result.is_full_table_scan) self.assertIn('EFFICIENT PARTITION KEY USAGE', result.performance_assessment) - def test_analyze_query_with_error(self): + async def test_analyze_query_with_error(self): """Test the analyze_query method when an error occurs.""" - # Mock the schema service to raise an exception - self.mock_schema_service.list_tables.side_effect = Exception('Test error') + self.mock_schema_service.list_tables = AsyncMock(side_effect=Exception('Test error')) - # Call the analyze_query method - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', 'SELECT * FROM users WHERE id = 1' ) - # Verify the result contains the error self.assertIn('Error analyzing query', result.performance_assessment) self.assertEqual(result.table_name, 'users') - def test_analyze_query_with_allow_filtering(self): + async def test_analyze_query_with_allow_filtering(self): """Test analyzing a query with ALLOW FILTERING.""" - # Mock the schema service responses table_info_mock = Mock() type(table_info_mock).name = PropertyMock(return_value='users') - self.mock_schema_service.list_tables.return_value = [table_info_mock] - self.mock_schema_service.describe_table.return_value = { - 'columns': [ - {'name': 'id', 'is_partition_key': True}, - {'name': 'name', 'is_partition_key': False}, - ], - 'partition_key': ['id'], - 'clustering_columns': [], - 'indexes': [], - } + self.mock_schema_service.list_tables = AsyncMock(return_value=[table_info_mock]) + self.mock_schema_service.describe_table = AsyncMock( + return_value={ + 'columns': [ + {'name': 'id', 'is_partition_key': True}, + {'name': 'name', 'is_partition_key': False}, + ], + 'partition_key': ['id'], + 'clustering_columns': [], + 'indexes': [], + } + ) - # Call the analyze_query method - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', "SELECT * FROM users WHERE name = 'test' ALLOW FILTERING" ) - # Verify the result self.assertTrue(result.uses_allow_filtering) self.assertIn('ALLOW FILTERING', result.performance_assessment) self.assertIn('Avoid using ALLOW FILTERING', ' '.join(result.recommendations)) - def test_analyze_query_with_secondary_index(self): + async def test_analyze_query_with_secondary_index(self): """Test analyzing a query that uses a secondary index.""" table_info_mock = Mock() type(table_info_mock).name = PropertyMock(return_value='users') - self.mock_schema_service.list_tables.return_value = [table_info_mock] - self.mock_schema_service.describe_table.return_value = { - 'columns': [ - {'name': 'id', 'is_partition_key': True}, - {'name': 'name', 'is_partition_key': False}, - ], - 'partition_key': ['id'], - 'clustering_columns': [], - 'indexes': [{'options': {'target': 'name'}}], - } + self.mock_schema_service.list_tables = AsyncMock(return_value=[table_info_mock]) + self.mock_schema_service.describe_table = AsyncMock( + return_value={ + 'columns': [ + {'name': 'id', 'is_partition_key': True}, + {'name': 'name', 'is_partition_key': False}, + ], + 'partition_key': ['id'], + 'clustering_columns': [], + 'indexes': [{'options': {'target': 'name'}}], + } + ) - # Call the analyze_query method - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', "SELECT * FROM users WHERE name = 'test'" ) - # Verify the result self.assertTrue(result.uses_secondary_index) self.assertIn('SECONDARY INDEX USAGE', result.performance_assessment) self.assertIn('Monitor the performance', ' '.join(result.recommendations)) - def test_analyze_query_table_not_found(self): + async def test_analyze_query_table_not_found(self): """Test analyzing a query when the table is not found.""" - # Mock the schema service responses - self.mock_schema_service.list_tables.return_value = [] + self.mock_schema_service.list_tables = AsyncMock(return_value=[]) - # Call the analyze_query method - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', 'SELECT * FROM users WHERE id = 1' ) - # Verify the result self.assertEqual(result.table_name, 'users') self.assertIn("Table 'users' not found", result.performance_assessment) self.assertIn('Verify the table name', result.recommendations[0]) - def test_analyze_query_unable_to_determine_table(self): + async def test_analyze_query_unable_to_determine_table(self): """Test analyzing a query when the table name cannot be determined.""" - # Call the analyze_query method with a malformed query - result = self.query_analysis_service.analyze_query( + result = await self.query_analysis_service.analyze_query( 'myks', - 'SELECT * WHERE id = 1', # Missing FROM clause + 'SELECT * WHERE id = 1', ) - # Verify the result self.assertEqual(result.table_name, '') self.assertIn('Unable to determine table name', result.performance_assessment) self.assertIn('Ensure the query follows standard CQL', result.recommendations[0]) + async def test_extract_where_conditions_no_where(self): + """Test extracting WHERE conditions from query without WHERE clause.""" + conditions = self.query_analysis_service._extract_where_conditions( + 'SELECT * FROM users' + ) + self.assertEqual(conditions, []) + if __name__ == '__main__': unittest.main() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index 750a3fc02b..b11a54250e 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -13,11 +13,11 @@ # and limitations under the License. """Unit tests for the server module.""" -import pytest - import unittest from unittest.mock import AsyncMock, Mock, patch +import pytest + from awslabs.amazon_keyspaces_mcp_server.consts import MAX_DISPLAY_ROWS from awslabs.amazon_keyspaces_mcp_server.models import ( KeyspaceInfo, @@ -458,12 +458,90 @@ async def test_handle_analyze_query_performance_error(self): 'mykeyspace', 'SELECT * FROM users' ) + async def test_handle_execute_query_mixed_case_unsafe(self): + """Test query validation with mixed case unsafe operations.""" + with self.assertRaises(Exception) as context: + await self.server._handle_execute_query( + 'mykeyspace', 'SeLeCt * FrOm users; DrOp TaBlE users;', self.mock_context + ) + self.assertIn('potentially unsafe operations', str(context.exception)) + + async def test_handle_execute_query_comment_with_unsafe_keyword(self): + """Test query with comment containing unsafe keyword is blocked.""" + with self.assertRaises(Exception) as context: + await self.server._handle_execute_query( + 'mykeyspace', 'SELECT * FROM users -- this is a DROP comment', self.mock_context + ) + self.assertIn('potentially unsafe operations', str(context.exception)) + + async def test_handle_execute_query_whitespace_variations(self): + """Test query validation with various whitespace.""" + with self.assertRaises(Exception) as context: + await self.server._handle_execute_query( + 'mykeyspace', ' \n\t SELECT * FROM users ; \n DROP TABLE users', self.mock_context + ) + self.assertIn('potentially unsafe operations', str(context.exception)) + + async def test_handle_execute_query_special_chars_in_columns(self): + """Test result formatting with special characters in column names.""" + query_results = { + 'columns': ['user_id', 'first-name', 'email@domain'], + 'rows': [{'user_id': 1, 'first-name': 'John', 'email@domain': 'test@example.com'}], + 'row_count': 1, + } + self.mock_data_service.execute_read_only_query.return_value = query_results + + result = await self.server._handle_execute_query( + 'mykeyspace', 'SELECT * FROM users', self.mock_context + ) + self.assertIn('user_id', result) + self.assertIn('first-name', result) + self.assertIn('email@domain', result) + + async def test_handle_execute_query_long_values(self): + """Test result formatting with very long column values.""" + long_text = 'x' * 1000 + query_results = { + 'columns': ['id', 'description'], + 'rows': [{'id': 1, 'description': long_text}], + 'row_count': 1, + } + self.mock_data_service.execute_read_only_query.return_value = query_results + + result = await self.server._handle_execute_query( + 'mykeyspace', 'SELECT * FROM users', self.mock_context + ) + self.assertIn(long_text, result) + + async def test_handle_execute_query_null_values_mixed(self): + """Test result formatting with null values in different positions.""" + query_results = { + 'columns': ['id', 'name', 'email'], + 'rows': [ + {'id': 1, 'name': None, 'email': 'test@example.com'}, + {'id': None, 'name': 'John', 'email': None}, + ], + 'row_count': 2, + } + self.mock_data_service.execute_read_only_query.return_value = query_results + + result = await self.server._handle_execute_query( + 'mykeyspace', 'SELECT * FROM users', self.mock_context + ) + self.assertIn('null', result) + self.assertEqual(result.count('null'), 3) + @patch('awslabs.amazon_keyspaces_mcp_server.server.UnifiedCassandraClient') @patch('awslabs.amazon_keyspaces_mcp_server.server.AppConfig') @pytest.mark.asyncio async def test_get_proxy(mock_app_config, mock_client_class): """Test the get_proxy function.""" + # Reset the global proxy + # pylint: disable=import-outside-toplevel + import awslabs.amazon_keyspaces_mcp_server.server as server_module + server_module._PROXY = None # pylint: disable=protected-access + # Set up the mocks mock_app_config_instance = Mock() mock_app_config.from_env.return_value = mock_app_config_instance diff --git a/src/amazon-keyspaces-mcp-server/tests/test_services.py b/src/amazon-keyspaces-mcp-server/tests/test_services.py index 7025be1af5..1612a1d9cc 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_services.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_services.py @@ -1,79 +1,72 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance -# with the License. A copy of the License is located at +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES -# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# or in the 'license' file accompanying this file. +# This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. +# See the License for the specific language governing permissions # and limitations under the License. """Unit tests for the services module.""" import unittest -from awslabs.amazon_keyspaces_mcp_server.models import ( - KeyspaceInfo, - TableInfo, -) -from awslabs.amazon_keyspaces_mcp_server.services import ( - DataService, - SchemaService, -) -from unittest.mock import Mock - - -class TestDataService(unittest.TestCase): +from unittest.mock import AsyncMock, Mock + +from awslabs.amazon_keyspaces_mcp_server.models import KeyspaceInfo, TableInfo +from awslabs.amazon_keyspaces_mcp_server.services import DataService, SchemaService + + +class TestDataService(unittest.IsolatedAsyncioTestCase): """Tests for the DataService class.""" def setUp(self): """Set up test fixtures.""" self.mock_client = Mock() self.mock_client.is_using_keyspaces.return_value = True - self.mock_client.execute_read_only_query.return_value = { - 'columns': ['id', 'name', 'value'], - 'rows': [{'id': 1, 'name': 'test', 'value': 100}], - 'row_count': 1, - } + self.mock_client.execute_read_only_query = AsyncMock( + return_value={ + 'columns': ['id', 'name', 'value'], + 'rows': [{'id': 1, 'name': 'test', 'value': 100}], + 'row_count': 1, + } + ) self.data_service = DataService(self.mock_client) - def test_execute_read_only_query_without_keyspace_qualifier(self): + async def test_execute_read_only_query_without_keyspace_qualifier(self): """Test executing a query without a keyspace qualifier.""" keyspace_name = 'my_keyspace' query = 'SELECT * FROM my_table' - result = self.data_service.execute_read_only_query(keyspace_name, query) + result = await self.data_service.execute_read_only_query(keyspace_name, query) - # Verify the client was called with the qualified query self.mock_client.execute_read_only_query.assert_called_once() call_args = self.mock_client.execute_read_only_query.call_args[0][0] self.assertEqual(call_args, 'SELECT * FROM my_keyspace.my_table') - - # Verify the result is returned correctly self.assertEqual(result['row_count'], 1) self.assertEqual(result['columns'], ['id', 'name', 'value']) self.assertEqual(len(result['rows']), 1) - def test_execute_read_only_query_with_keyspace_qualifier(self): + async def test_execute_read_only_query_with_keyspace_qualifier(self): """Test executing a query that already has a keyspace qualifier.""" keyspace_name = 'my_keyspace' query = 'SELECT * FROM my_keyspace.my_table' - result = self.data_service.execute_read_only_query(keyspace_name, query) + result = await self.data_service.execute_read_only_query(keyspace_name, query) - # Verify the client was called with the original query self.mock_client.execute_read_only_query.assert_called_once_with(query) - - # Verify the result is returned correctly self.assertEqual(result['row_count'], 1) - def test_execute_read_only_query_with_complex_query(self): + async def test_execute_read_only_query_with_complex_query(self): """Test executing a more complex query.""" keyspace_name = 'my_keyspace' query = 'SELECT id, name FROM my_table WHERE id = 1 ORDER BY name' - self.data_service.execute_read_only_query(keyspace_name, query) + await self.data_service.execute_read_only_query(keyspace_name, query) - # Verify the client was called with the qualified query self.mock_client.execute_read_only_query.assert_called_once() call_args = self.mock_client.execute_read_only_query.call_args[0][0] self.assertEqual( @@ -81,7 +74,7 @@ def test_execute_read_only_query_with_complex_query(self): ) -class TestSchemaService(unittest.TestCase): +class TestSchemaService(unittest.IsolatedAsyncioTestCase): """Tests for the SchemaService class.""" def setUp(self): @@ -90,67 +83,51 @@ def setUp(self): self.mock_client.is_using_keyspaces.return_value = True self.schema_service = SchemaService(self.mock_client) - def test_list_keyspaces(self): + async def test_list_keyspaces(self): """Test listing keyspaces.""" - # Set up mock return value mock_keyspaces = [KeyspaceInfo(name='system'), KeyspaceInfo(name='my_keyspace')] - self.mock_client.list_keyspaces.return_value = mock_keyspaces + self.mock_client.list_keyspaces = AsyncMock(return_value=mock_keyspaces) - # Call the method - result = self.schema_service.list_keyspaces() + result = await self.schema_service.list_keyspaces() - # Verify the client was called self.mock_client.list_keyspaces.assert_called_once() - - # Verify the result self.assertEqual(len(result), 2) self.assertEqual(result[0].name, 'system') self.assertEqual(result[1].name, 'my_keyspace') - def test_list_tables(self): + async def test_list_tables(self): """Test listing tables in a keyspace.""" - # Set up mock return value mock_tables = [ TableInfo(name='users', keyspace='my_keyspace'), TableInfo(name='products', keyspace='my_keyspace'), ] - self.mock_client.list_tables.return_value = mock_tables + self.mock_client.list_tables = AsyncMock(return_value=mock_tables) - # Call the method - result = self.schema_service.list_tables('my_keyspace') + result = await self.schema_service.list_tables('my_keyspace') - # Verify the client was called with the correct keyspace self.mock_client.list_tables.assert_called_once_with('my_keyspace') - - # Verify the result self.assertEqual(len(result), 2) self.assertEqual(result[0].name, 'users') self.assertEqual(result[1].name, 'products') - def test_describe_keyspace(self): + async def test_describe_keyspace(self): """Test describing a keyspace.""" - # Set up mock return value mock_keyspace_details = { 'name': 'my_keyspace', 'replication': {'class': 'NetworkTopologyStrategy', 'dc1': '3'}, 'durable_writes': True, } - self.mock_client.describe_keyspace.return_value = mock_keyspace_details + self.mock_client.describe_keyspace = AsyncMock(return_value=mock_keyspace_details) - # Call the method - result = self.schema_service.describe_keyspace('my_keyspace') + result = await self.schema_service.describe_keyspace('my_keyspace') - # Verify the client was called with the correct keyspace self.mock_client.describe_keyspace.assert_called_once_with('my_keyspace') - - # Verify the result self.assertEqual(result['name'], 'my_keyspace') self.assertEqual(result['replication']['class'], 'NetworkTopologyStrategy') self.assertTrue(result['durable_writes']) - def test_describe_table(self): + async def test_describe_table(self): """Test describing a table.""" - # Set up mock return value mock_table_details = { 'name': 'users', 'keyspace': 'my_keyspace', @@ -161,15 +138,11 @@ def test_describe_table(self): 'partition_key': ['user_id'], 'clustering_columns': [], } - self.mock_client.describe_table.return_value = mock_table_details + self.mock_client.describe_table = AsyncMock(return_value=mock_table_details) - # Call the method - result = self.schema_service.describe_table('my_keyspace', 'users') + result = await self.schema_service.describe_table('my_keyspace', 'users') - # Verify the client was called with the correct keyspace and table self.mock_client.describe_table.assert_called_once_with('my_keyspace', 'users') - - # Verify the result self.assertEqual(result['name'], 'users') self.assertEqual(result['keyspace'], 'my_keyspace') self.assertEqual(len(result['columns']), 2) From dc5938f0e680a593f77c9b2da1db18e61583ebab Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 20:32:36 -0800 Subject: [PATCH 15/24] Increase code coverage --- .../llm_context.py | 4 +- .../tests/test_client.py | 245 ++++++++++++++++++ .../tests/test_llm_context.py | 70 +++++ .../tests/test_services.py | 5 + 4 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 src/amazon-keyspaces-mcp-server/tests/test_llm_context.py diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py index 935f151e0c..3e09868bf1 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py @@ -83,7 +83,7 @@ def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> "entity they represent." ), } - + # Add guidance for empty results if not _tables: tables_guidance["empty_result_interpretation"] = ( @@ -92,7 +92,7 @@ def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> "the keyspace might not exist, use the listKeyspaces tool to verify the " "keyspace name is correct before concluding it's empty." ) - + context['tables_guidance'] = tables_guidance return dict_to_markdown(context) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_client.py b/src/amazon-keyspaces-mcp-server/tests/test_client.py index f381acf158..12bca772dd 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_client.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_client.py @@ -613,6 +613,251 @@ async def test_get_session_connection_failure(self): self.assertIn('Failed to connect to Cassandra cluster', str(context.exception)) + async def test_list_keyspaces_with_invalid_replication_factor_value_error(self): + """Test listing keyspaces with invalid replication_factor (ValueError).""" + mock_row = Mock() + mock_row.keyspace_name = 'test_ks' + mock_row.replication = {'class': 'SimpleStrategy', 'replication_factor': 'invalid'} + + self.mock_session.execute.return_value = [mock_row] + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.cassandra_config) + keyspaces = await client.list_keyspaces() + + self.assertEqual(len(keyspaces), 1) + self.assertEqual(keyspaces[0].replication_factor, 0) + + async def test_list_keyspaces_with_none_replication_factor_type_error(self): + """Test listing keyspaces with None replication_factor (TypeError).""" + mock_row = Mock() + mock_row.keyspace_name = 'test_ks' + mock_row.replication = {'class': 'SimpleStrategy', 'replication_factor': None} + + self.mock_session.execute.return_value = [mock_row] + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.cassandra_config) + keyspaces = await client.list_keyspaces() + + self.assertEqual(len(keyspaces), 1) + self.assertEqual(keyspaces[0].replication_factor, 0) + + def test_build_service_characteristics(self): + """Test building service characteristics for Keyspaces.""" + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster'): + client = UnifiedCassandraClient(self.keyspaces_config) + characteristics = client._build_service_characteristics() # pylint: disable=protected-access + + self.assertIn('write_throughput_limitation', characteristics) + self.assertIn('implementation_notes', characteristics) + self.assertIn('response_guidance', characteristics) + self.assertIn('do_not_mention', characteristics['response_guidance']) + self.assertIn('preferred_terminology', characteristics['response_guidance']) + self.assertEqual(len(characteristics['response_guidance']['do_not_mention']), 3) + self.assertEqual(len(characteristics['response_guidance']['preferred_terminology']), 3) + + def test_close_without_session(self): + """Test closing the client when no session exists.""" + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster'): + client = UnifiedCassandraClient(self.cassandra_config) + client.close() # Should not raise an exception + + async def test_close_without_cluster(self): + """Test closing the client when session exists but cluster is None.""" + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + self.mock_session.cluster = None + + client = UnifiedCassandraClient(self.cassandra_config) + await client.get_session() + client.close() + + self.mock_session.shutdown.assert_called_once() + + async def test_describe_table_with_capacity_mode(self): + """Test describing a Keyspaces table with capacity mode information.""" + mock_table_row = Mock() + mock_table_row.table_name = 'users' + mock_table_row.keyspace_name = 'mykeyspace' + + mock_capacity_row = Mock() + mock_capacity_row.custom_properties = { + 'capacity_mode': 'PROVISIONED', + 'read_capacity_units': '100', + 'write_capacity_units': '50' + } + + def mock_execute(query, _params=None): + if 'tables' in query and 'system_schema_mcs' not in query: + result = Mock() + result.one.return_value = mock_table_row + return result + elif 'columns' in query: + return [] + elif 'indexes' in query: + return [] + elif 'system_schema_mcs' in query: + result = Mock() + result.one.return_value = mock_capacity_row + return result + return [] + + self.mock_session.execute = mock_execute + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.keyspaces_config) + table_details = await client.describe_table('mykeyspace', 'users') + + self.assertEqual(table_details['capacity_mode'], 'PROVISIONED') + self.assertEqual(table_details['read_capacity_units'], 100) + self.assertEqual(table_details['write_capacity_units'], 50) + + async def test_execute_read_only_query_with_column_error(self): + """Test query execution when getting column value raises an error.""" + mock_column_names = ['id', 'bad_column'] + mock_row = Mock() + mock_row.id = 1 + type(mock_row).bad_column = property(lambda self: (_ for _ in ()).throw(ValueError('Bad column'))) + + mock_result_set = Mock() + mock_result_set.column_names = mock_column_names + mock_result_set.__iter__ = lambda self: iter([mock_row]) + mock_result_set.response_future = None + + self.mock_session.execute.return_value = mock_result_set + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.cassandra_config) + result = await client.execute_read_only_query('SELECT * FROM users') + + self.assertEqual(result['rows'][0]['id'], 1) + self.assertIsNone(result['rows'][0]['bad_column']) + + async def test_describe_table_without_capacity_mode(self): + """Test table without capacity_mode in custom_properties.""" + mock_table_row = Mock() + mock_table_row.table_name = 'users' + mock_table_row.keyspace_name = 'mykeyspace' + + mock_capacity_row = Mock() + mock_capacity_row.custom_properties = {'other_property': 'value'} + + def mock_execute(query, _params=None): + if 'tables' in query and 'system_schema_mcs' not in query: + result = Mock() + result.one.return_value = mock_table_row + return result + elif 'columns' in query: + return [] + elif 'indexes' in query: + return [] + elif 'system_schema_mcs' in query: + result = Mock() + result.one.return_value = mock_capacity_row + return result + return [] + + self.mock_session.execute = mock_execute + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.keyspaces_config) + table_details = await client.describe_table('mykeyspace', 'users') + + self.assertNotIn('capacity_mode', table_details) + self.assertNotIn('read_capacity_units', table_details) + self.assertNotIn('write_capacity_units', table_details) + + + async def test_describe_table_with_on_demand_capacity(self): + """Test table with ON_DEMAND capacity mode.""" + mock_table_row = Mock() + mock_table_row.table_name = 'users' + mock_table_row.keyspace_name = 'mykeyspace' + + mock_capacity_row = Mock() + mock_capacity_row.custom_properties = {'capacity_mode': 'ON_DEMAND'} + + def mock_execute(query, _params=None): + if 'tables' in query and 'system_schema_mcs' not in query: + result = Mock() + result.one.return_value = mock_table_row + return result + elif 'columns' in query: + return [] + elif 'indexes' in query: + return [] + elif 'system_schema_mcs' in query: + result = Mock() + result.one.return_value = mock_capacity_row + return result + return [] + + self.mock_session.execute = mock_execute + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.keyspaces_config) + table_details = await client.describe_table('mykeyspace', 'users') + + self.assertEqual(table_details['capacity_mode'], 'ON_DEMAND') + self.assertNotIn('read_capacity_units', table_details) + self.assertNotIn('write_capacity_units', table_details) + + async def test_describe_table_provisioned_missing_capacity_units(self): + """Test PROVISIONED mode without required capacity units raises error.""" + mock_table_row = Mock() + mock_table_row.table_name = 'users' + mock_table_row.keyspace_name = 'mykeyspace' + + mock_capacity_row = Mock() + mock_capacity_row.custom_properties = {'capacity_mode': 'PROVISIONED'} + + def mock_execute(query, _params=None): + if 'tables' in query and 'system_schema_mcs' not in query: + result = Mock() + result.one.return_value = mock_table_row + return result + elif 'columns' in query: + return [] + elif 'indexes' in query: + return [] + elif 'system_schema_mcs' in query: + result = Mock() + result.one.return_value = mock_capacity_row + return result + return [] + + self.mock_session.execute = mock_execute + + with patch('awslabs.amazon_keyspaces_mcp_server.client.Cluster') as mock_cluster_class: + mock_cluster_instance = mock_cluster_class.return_value + mock_cluster_instance.connect.return_value = self.mock_session + + client = UnifiedCassandraClient(self.keyspaces_config) + table_details = await client.describe_table('mykeyspace', 'users') + + self.assertNotIn('read_capacity_units', table_details) + self.assertNotIn('write_capacity_units', table_details) + if __name__ == '__main__': unittest.main() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py b/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py new file mode 100644 index 0000000000..82a32d6e35 --- /dev/null +++ b/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py @@ -0,0 +1,70 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# 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. + +"""Tests for llm_context module.""" + +import unittest + +from awslabs.amazon_keyspaces_mcp_server.llm_context import ( + build_query_result_context, + build_table_details_context, + dict_to_markdown, +) + + +class TestLlmContext(unittest.TestCase): + """Test cases for llm_context functions.""" + + def test_build_table_details_context_with_keyspaces_context(self): + """Test building table context with Keyspaces-specific metadata.""" + table_details = { + 'keyspace_name': 'test_ks', + 'table_name': 'test_table', + '_keyspaces_context': { + 'service_characteristics': 'serverless' + } + } + result = build_table_details_context(table_details) + self.assertIn('Service Characteristics', result) + self.assertIn('serverless', result) + + def test_build_query_result_context_large_result(self): + """Test query result context with large result set.""" + query_results = { + 'row_count': 150, + 'columns': ['id'], + 'rows': [] + } + result = build_query_result_context(query_results) + self.assertIn('Large Result', result) + + def test_dict_to_markdown_with_list_of_dicts(self): + """Test markdown conversion with nested list of dicts.""" + data = {'items': [{'name': 'item1'}, {'name': 'item2'}]} + result = dict_to_markdown(data) + self.assertIn('Items', result) + self.assertIn('Name', result) + + def test_dict_to_markdown_with_list_of_strings(self): + """Test markdown conversion with list of strings (covers line 341).""" + data = {'features': ['feature1', 'feature2', 'feature3']} + result = dict_to_markdown(data) + self.assertIn('Features', result) + self.assertIn('- feature1', result) + self.assertIn('- feature2', result) + self.assertIn('- feature3', result) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_services.py b/src/amazon-keyspaces-mcp-server/tests/test_services.py index 1612a1d9cc..6085ea84e7 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_services.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_services.py @@ -148,3 +148,8 @@ async def test_describe_table(self): self.assertEqual(len(result['columns']), 2) self.assertEqual(result['columns'][0]['name'], 'user_id') self.assertEqual(result['partition_key'], ['user_id']) + + +if __name__ == '__main__': + unittest.main() + From 0a49d7e7bbffa639530c1130611360f5bd688529 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 20:32:49 -0800 Subject: [PATCH 16/24] Correct logic for provisioned schema table attributes --- .../awslabs/amazon_keyspaces_mcp_server/client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py index 36ba086e6a..b459856375 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py @@ -328,12 +328,13 @@ async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, table_details['capacity_mode'] = props['capacity_mode'] if props['capacity_mode'] == 'PROVISIONED': - table_details['read_capacity_units'] = int( - props.get('read_capacity_units', 0) - ) - table_details['write_capacity_units'] = int( - props.get('write_capacity_units', 0) - ) + if 'read_capacity_units' not in props or 'write_capacity_units' not in props: + raise RuntimeError( + f'PROVISIONED capacity mode requires both read_capacity_units ' + f'and write_capacity_units for table {keyspace_name}.{table_name}' + ) + table_details['read_capacity_units'] = int(props['read_capacity_units']) + table_details['write_capacity_units'] = int(props['write_capacity_units']) except (RuntimeError, ValueError, AttributeError) as e: # Ignore errors when trying to get capacity information logger.warning( From 5c658d00b6f9b835305de809ae5df6f34ad197ab Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 21:03:30 -0800 Subject: [PATCH 17/24] Add MCP resources from Cassandra/Keyspaces documentation --- .../amazon_keyspaces_mcp_server/server.py | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 29ef4bab79..471ad43879 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -73,9 +73,260 @@ 2. Use describeTable to understand table schemas before querying 3. Only SELECT queries are permitted for data safety 4. Use analyzeQueryPerformance to optimize queries before execution + +## Resources + +- **keyspaces://developer-guide**: Complete developer guide with best practices +- **keyspaces://cql-reference**: CQL language reference for Keyspaces +- **keyspaces://api-reference**: AWS Keyspaces API documentation +- **keyspaces://streams-api**: Keyspaces Streams API reference +- **keyspaces://code-examples**: Sample code repository """, ) + +@mcp.resource('keyspaces://developer-guide') +def get_developer_guide() -> str: + """Amazon Keyspaces Developer Guide with best practices and tutorials.""" + return """# Amazon Keyspaces Developer Guide + +Complete guide for developing applications with Amazon Keyspaces. + +**Documentation**: https://docs.aws.amazon.com/pdfs/keyspaces/latest/devguide/AmazonKeyspaces.pdf + +## Key Topics + +### Getting Started +- Setting up credentials and connections +- Creating keyspaces and tables +- Loading data + +### Data Modeling +- Partition key design +- Clustering column strategies +- Denormalization patterns + +### Performance +- Read/write capacity modes +- Auto-scaling configuration +- Query optimization + +### Security +- IAM authentication +- Encryption at rest and in transit +- VPC endpoints + +### Monitoring +- CloudWatch metrics +- CloudTrail logging +- Point-in-time recovery + +Refer to the full PDF for detailed examples and best practices. +""" + + +@mcp.resource('keyspaces://cql-reference') +def get_cql_reference() -> str: + """CQL language reference for Amazon Keyspaces.""" + return """# Amazon Keyspaces CQL Reference + +Cassandra Query Language (CQL) syntax and operations supported by Keyspaces. + +**Documentation**: https://docs.aws.amazon.com/pdfs/keyspaces/latest/devguide/AmazonKeyspaces.pdf + +## Supported CQL Operations + +### Data Definition (DDL) +- CREATE/ALTER/DROP KEYSPACE +- CREATE/ALTER/DROP TABLE +- CREATE/DROP INDEX + +### Data Manipulation (DML) +- SELECT (with WHERE, ORDER BY, LIMIT) +- INSERT, UPDATE, DELETE +- BATCH operations + +### Data Types +- Primitive: text, int, bigint, boolean, decimal, timestamp, uuid +- Collections: list, set, map +- Special: frozen, tuple + +### Limitations +- No materialized views +- No user-defined functions +- No aggregate functions (COUNT, SUM) +- No TRUNCATE TABLE + +See full PDF for complete syntax and examples. +""" + + +@mcp.resource('keyspaces://api-reference') +def get_api_reference() -> str: + """AWS Keyspaces API Reference for management operations.""" + return """# Amazon Keyspaces API Reference + +Complete API documentation for Amazon Keyspaces management operations. + +**Documentation**: https://docs.aws.amazon.com/pdfs/keyspaces/latest/APIReference/keyspaces-api.pdf + +## Key API Operations + +### Keyspace Management +- CreateKeyspace, DeleteKeyspace, GetKeyspace, ListKeyspaces + +### Table Management +- CreateTable, DeleteTable, GetTable, UpdateTable, ListTables, RestoreTable + +### Configuration +- GetTableAutoScalingSettings +- TagResource, UntagResource, ListTagsForResource + +### Capacity Modes +- PAY_PER_REQUEST: Serverless, pay per request +- PROVISIONED: Fixed capacity with auto-scaling support + +### Encryption +- AWS_OWNED_KMS_KEY (default) +- CUSTOMER_MANAGED_KMS_KEY + +### Point-in-Time Recovery +- Continuous backups for up to 35 days +- Restore to any point within recovery window + +For detailed schemas, parameters, and examples, refer to the full PDF documentation. +""" + + +@mcp.resource('keyspaces://streams-api') +def get_streams_api() -> str: + """Amazon Keyspaces Streams API Reference for change data capture.""" + return """# Amazon Keyspaces Streams API Reference + +API for capturing and processing change data from Keyspaces tables. + +**Documentation**: https://docs.aws.amazon.com/pdfs/keyspaces/latest/StreamsAPIReference/keyspaces-streams-api.pdf + +## Overview + +Keyspaces Streams captures item-level changes (inserts, updates, deletes) in near real-time. + +## Key Operations + +### Stream Management +- GetRecords: Read change records from a stream +- DescribeStream: Get stream metadata +- ListStreams: List available streams + +### Use Cases +- Real-time analytics +- Data replication +- Audit logging +- Event-driven architectures + +### Integration +- AWS Lambda triggers +- Kinesis Data Streams +- Custom consumers + +Refer to the full PDF for detailed API specifications and examples. +""" + + +@mcp.resource('keyspaces://code-examples') +def get_code_examples() -> str: + """Amazon Keyspaces code examples and sample applications.""" + return """# Amazon Keyspaces Code Examples + +Sample code and reference implementations for common use cases. + +**Repository**: https://github.com/aws-samples/amazon-keyspaces-examples + +## Available Examples + +### Connection Patterns +- Python, Java, Node.js drivers +- IAM authentication +- SSL/TLS configuration + +### Data Operations +- CRUD operations +- Batch processing +- Pagination + +### Advanced Features +- Point-in-time recovery +- Auto-scaling setup +- Multi-region replication + +### Integration Examples +- Lambda functions +- ECS/EKS deployments +- Spring Boot applications + +Clone the repository for complete, runnable examples with detailed README files. +""" + + +@mcp.resource('cassandra://cql-reference') +def get_cassandra_cql_reference() -> str: + """Apache Cassandra CQL reference documentation.""" + return """# Apache Cassandra CQL Reference + +Complete CQL language reference for Apache Cassandra. + +**Documentation**: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/index.html + +## Key Sections + +### CQL Basics +- Definitions: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/definitions.html +- Data Types: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/types.html + +### Operations +- DDL (Data Definition): https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/ddl.html +- DML (Data Manipulation): https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/dml.html +- Operators: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/operators.html + +### Advanced Features +- Indexing: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/indexing/indexing-concepts.html +- Materialized Views: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/mvs.html +- Functions: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/functions.html +- JSON Support: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/json.html + +### Reference +- CQL Commands: https://cassandra.apache.org/doc/trunk/cassandra/reference/cql-commands/commands-toc.html +- Single File Reference: https://cassandra.apache.org/doc/trunk/cassandra/developing/cql/cql_singlefile.html +""" + + +@mcp.resource('cassandra://data-modeling') +def get_cassandra_data_modeling() -> str: + """Apache Cassandra data modeling guide.""" + return """# Apache Cassandra Data Modeling + +Comprehensive guide to data modeling in Cassandra. + +**Documentation**: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/index.html + +## Key Topics + +### Conceptual Modeling +- Introduction: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/intro.html +- Concepts: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_conceptual.html + +### Design Process +- RDBMS Design: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_rdbms.html +- Query Patterns: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_queries.html +- Logical Modeling: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_logical.html +- Physical Modeling: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_physical.html + +### Best Practices +- Evaluation: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_refining.html +- Schema Definition: https://cassandra.apache.org/doc/trunk/cassandra/developing/data-modeling/data-modeling_schema.html +""" + + # Global handle to hold the proxy to the specific database client _PROXY = None From eeba09a0a89ec45583a64aa42e3aac61fdd53cd2 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 21:07:31 -0800 Subject: [PATCH 18/24] Modularize llm context --- .../llm_context.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py index 3e09868bf1..097274e786 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py @@ -34,15 +34,10 @@ def build_list_keyspaces_context(_keyspaces: List[KeyspaceInfo]) -> str: ), "limitations": ( "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " - "Unsupported features include materialized views, " - "indexes, aggregate functions like COUNT and SUM, prepared statements " - "for DDL operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, " - "the inequality operator for user-defined types, or the IN keyword in " - "INSERT and UPDATE statements. Keyspaces uses AWS IAM for authentication " - "and authorization, and not Cassandra's security configuration and " - "commands. Additionally, some operations that are synchronous in Cassandra " - "are asynchronous in Keyspaces, such as DDL operations and range delete " - "operations." + "For the most current list of unsupported features and CQL limitations, " + "refer to the CQL Reference resource (keyspaces://cql-reference) which " + "includes up-to-date information on supported operations, data types, " + "and known restrictions." ), "replication_strategy": ( "In Cassandra, common replication strategies include SimpleStrategy and " @@ -69,14 +64,9 @@ def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> # Add table-specific guidance tables_guidance = { "data_modeling": ( - "In Cassandra, tables are containers for related data, similar to tables " - "in relational databases. However, Cassandra tables are optimized for " - "specific access patterns based on their primary key design. The primary " - "key determines how data is distributed physically in the database, and " - "the attributes that can be specified for efficient query execution. " - "Primary keys consist of a partition key (which determines data " - "distribution) and optional cluster columns which determine how data is " - "ordered within a partition." + "For detailed information on Cassandra data modeling, primary keys, " + "partition keys, and clustering columns, refer to the Developer Guide " + "resource (keyspaces://developer-guide)." ), "naming_conventions": ( "Table names typically use snake_case and should be descriptive of the " @@ -285,21 +275,16 @@ def build_amazon_keyspaces_knowledge() -> Dict[str, str]: """Provide Amazon Keyspaces specific knowledge.""" knowledge = { "compatibility": ( - "Amazon Keyspaces is compatible with Apache Cassandra 3.11. This means " - "that it supports most of the same CQL language features and is " - "driver-protocol compatible with Cassandra 3.11." + "For current compatibility information with Apache Cassandra, refer to " + "the Developer Guide resource (keyspaces://developer-guide)." ), "differences_from_cassandra": ( - "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " - "Unsupported features include materialized views, indexes, " - "aggregate functions like COUNT and SUM, prepared statements for DDL " - "operations, DROP COLUMN, TRUNCATE TABLE, user-defined functions, the " - "inequality operator for user-defined types, or the IN keyword in INSERT " - "and UPDATE statements. Keyspaces uses AWS IAM for authentication and " - "authorization, and not Cassandra's security configuration and commands. " - "Additionally, some operations that are synchronous in Cassandra are " - "asynchronous in Keyspaces, such as DDL operations and range delete " - "operations." + "For Apache Cassandra features and capabilities, refer to the official " + "Apache Cassandra documentation at https://cassandra.apache.org/doc/. " + "For differences between Amazon Keyspaces and Apache Cassandra, including " + "unsupported features and operational differences, refer to the CQL " + "Reference resource (keyspaces://cql-reference) and Developer Guide " + "resource (keyspaces://developer-guide)." ), } From b70d1813a132ae53a53e41ab8e7715a000efee50 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 21:08:31 -0800 Subject: [PATCH 19/24] Fix pylint warnings --- .../amazon_keyspaces_mcp_server/server.py | 18 +++++++++--------- .../tests/test_client.py | 8 ++++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 471ad43879..944bca0157 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -371,7 +371,7 @@ async def list_keyspaces( description='Lists all tables in a specified keyspace - args: keyspace', ) async def list_tables( - input: KeyspaceInput, + input: KeyspaceInput, # pylint: disable=redefined-builtin ctx: Optional[Context] = None, ) -> str: """Lists all tables in a specified keyspace.""" @@ -384,7 +384,7 @@ async def list_tables( description='Gets detailed information about a keyspace - args: keyspace', ) async def describe_keyspace( - input: KeyspaceInput, + input: KeyspaceInput, # pylint: disable=redefined-builtin ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a keyspace.""" @@ -397,7 +397,7 @@ async def describe_keyspace( description='Gets detailed information about a table - args: keyspace, table', ) async def describe_table( - input: TableInput, + input: TableInput, # pylint: disable=redefined-builtin ctx: Optional[Context] = None, ) -> str: """Gets detailed information about a table.""" @@ -410,7 +410,7 @@ async def describe_table( description='Executes a read-only SELECT query against the database - args: keyspace, query', ) async def execute_query( - input: QueryInput, + input: QueryInput, # pylint: disable=redefined-builtin ctx: Optional[Context] = None, ) -> str: """Executes a read-only (SELECT) query against the database.""" @@ -423,7 +423,7 @@ async def execute_query( description='Analyzes the performance characteristics of a CQL query - args: keyspace, query', ) async def analyze_query_performance( - input: QueryInput, + input: QueryInput, # pylint: disable=redefined-builtin ctx: Optional[Context] = None, ) -> str: """Analyzes the performance characteristics of a CQL query.""" @@ -732,17 +732,17 @@ async def _handle_analyze_query_performance( def main(): """Run the MCP server.""" - import asyncio - + import asyncio # pylint: disable=import-outside-toplevel + # Validate connection before starting server try: proxy = asyncio.run(get_proxy()) asyncio.run(proxy.schema_service.cassandra_client.get_session()) logger.success('Successfully validated database connection') - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(f'Failed to connect to database: {e}') sys.exit(1) - + mcp.run() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_client.py b/src/amazon-keyspaces-mcp-server/tests/test_client.py index 12bca772dd..ae54bc714f 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_client.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_client.py @@ -222,7 +222,9 @@ async def test_ssl_context_load_error(self, mock_ssl, mock_cluster_class): mock_ssl.SSLError = ssl.SSLError # Make load_verify_locations raise an exception - mock_ssl_context.load_verify_locations.side_effect = FileNotFoundError('Certificate not found') + mock_ssl_context.load_verify_locations.side_effect = FileNotFoundError( + 'Certificate not found' + ) # Create the client and get session client = UnifiedCassandraClient(self.keyspaces_config) @@ -728,7 +730,9 @@ async def test_execute_read_only_query_with_column_error(self): mock_column_names = ['id', 'bad_column'] mock_row = Mock() mock_row.id = 1 - type(mock_row).bad_column = property(lambda self: (_ for _ in ()).throw(ValueError('Bad column'))) + type(mock_row).bad_column = property( + lambda self: (_ for _ in ()).throw(ValueError('Bad column')) + ) mock_result_set = Mock() mock_result_set.column_names = mock_column_names From 4ad7270f815e49413b6d387efcf887868b8a7228 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 21:44:32 -0800 Subject: [PATCH 20/24] Add Resource arn input model --- .../awslabs/amazon_keyspaces_mcp_server/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py index a7a17f9309..cad4eb1d36 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py @@ -176,3 +176,14 @@ def sanitize_query(cls, v: str) -> str: v = re.sub(r'[\u200B-\u200D\uFEFF\u0000-\u001F\u007F-\u009F]', '', v) return v.strip() + +class ResourceArnInput(BaseModel): + """Validated resource ARN input.""" + resource_arn: str = Field( + ..., + min_length=1, + max_length=1000, + description='ARN of the resource (keyspace or table)' + ) + + From 2742b4be9880024ee9a6f488a8141ebc093eeef5 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Thu, 4 Dec 2025 21:57:17 -0800 Subject: [PATCH 21/24] Update documentation --- src/amazon-keyspaces-mcp-server/README.md | 43 ++++++++++--------- .../pyproject.toml | 1 + 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/README.md b/src/amazon-keyspaces-mcp-server/README.md index a554105f9a..2230cf567f 100644 --- a/src/amazon-keyspaces-mcp-server/README.md +++ b/src/amazon-keyspaces-mcp-server/README.md @@ -1,10 +1,10 @@ -# AWS Labs amazon-keyspaces MCP Server +# AWS Labs Amazon Keyspaces MCP Server An Amazon Keyspaces (for Apache Cassandra) MCP server for interacting with Amazon Keyspaces and Apache Cassandra. ## Overview -The Amazon Keyspaces MCP server implements the Model Context Protocol (MCP) to enable AI assistants like Amazon Q to +The Amazon Keyspaces MCP server implements the Model Context Protocol (MCP) to enable AI assistants like Kiro to interact with Amazon Keyspaces or Apache Cassandra databases through natural language. This server allows you to explore database schemas, execute queries, and analyze query performance without having to write CQL code directly. @@ -34,7 +34,7 @@ Here are some example prompts that this MCP server can help with: ### Prerequisites -- Python 3.10 or 3.11 (Python 3.12+ is not fully supported due to asyncore module removal) +- Python 3.10+ - Access to an Amazon Keyspaces instance or Apache Cassandra cluster that supports password authentication - Appropriate Cassandra log-in credentials - Starfield digital certificate (required for Amazon Keyspaces) @@ -123,30 +123,36 @@ After installation, you can run the server directly: awslabs.amazon-keyspaces-mcp-server ``` -## Configuring Amazon Q to Use the MCP Server +## Configuring Kiro CLI to Use the MCP Server -To use the Amazon Keyspaces MCP server with Amazon Q CLI, you need to configure it in your Q configuration file. +To use the Amazon Keyspaces MCP server with Kiro CLI, you need to configure it in your Kiro configuration file. -### Configuration for Amazon Q CLI +### Configuration for Kiro CLI -Edit the Q configuration file at `~/.aws/amazonq/mcp.json`: +Edit the Kiro configuration file at `~/.kiro/settings/mcp.json`: ```json { - "mcpServers": [ - { - "name": "keyspaces-mcp", - "command": "awslabs.amazon-keyspaces-mcp-server", - "args": [], - "env": {} + "mcpServers": { + "aws-labs-keyspaces-mcp" : { + "command" : "uvx", + "args" : ["awslabs.amazon-keyspaces-mcp-server@latest"], + "env" : { + "AWS_PROFILE" : "your-aws-profile", + "AWS_REGION" : "us-east-1", + "FASTMCP_LOG_LEVEL" : "ERROR" + }, + "disabled" : false, + "autoApprove" : [] } - ] + } + } ``` ### Windows Installation -For Windows users, the MCP server configuration format is slightly different. Edit your MCP configuration file (e.g., `~/.aws/amazonq/mcp.json` for Amazon Q CLI) with the following format: +For Windows users, the MCP server configuration format is slightly different. Edit your MCP configuration file (e.g., `~/.kiro/settings/mcp.json` for Kiro CLI) with the following format: ```json { @@ -176,7 +182,7 @@ For Windows users, the MCP server configuration format is slightly different. Ed If the file doesn't exist yet or doesn't have an `mcpServers` section, create it with the structure shown above. -Now when you use Q Chat by running `q chat`, it will automatically connect to your Keyspaces MCP server. +Now when you use Kiro CLI by running `kiro-cli chat`, it will automatically connect to your Keyspaces MCP server. ## Available Tools @@ -207,11 +213,6 @@ invoke AWS SDK operations on your behalf, including mutating operations. - For Amazon Keyspaces, verify that the Starfield certificate is correctly installed in the `.keyspaces-mcp/certs` directory. - If you get SSL/TLS errors, check that the certificate path is correct and the certificate is valid. -### Python Version Compatibility - -- The MCP server works best with Python 3.10 or 3.11. -- Python 3.12+ may have issues due to the removal of the asyncore module which the Cassandra driver depends on. - ### Cassandra Driver Issues If you encounter issues with the Cassandra driver: diff --git a/src/amazon-keyspaces-mcp-server/pyproject.toml b/src/amazon-keyspaces-mcp-server/pyproject.toml index cab48d02d6..39ad6501d2 100644 --- a/src/amazon-keyspaces-mcp-server/pyproject.toml +++ b/src/amazon-keyspaces-mcp-server/pyproject.toml @@ -19,6 +19,7 @@ authors = [ {name = "Amazon Web Services"}, {name = "AWSLabs MCP", email="203918161+awslabs-mcp@users.noreply.github.com"}, {name = "Joel Shepherd", email="shepherd@amazon.com"}, + {name = "Michael Christensen", email="mlynnc@amazon.com"}, ] classifiers = [ "License :: OSI Approved :: Apache Software License", From 930060eb95c5864384370422dd23184bf312e8da Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Mon, 8 Dec 2025 11:13:35 -0800 Subject: [PATCH 22/24] chore: apply pre-commit fixes (EOF, trailing whitespace) --- src/amazon-keyspaces-mcp-server/README.md | 2 +- .../amazon_keyspaces_mcp_server/client.py | 37 ++-- .../amazon_keyspaces_mcp_server/config.py | 3 +- .../amazon_keyspaces_mcp_server/exceptions.py | 2 +- .../llm_context.py | 204 +++++++++--------- .../amazon_keyspaces_mcp_server/models.py | 37 +--- .../amazon_keyspaces_mcp_server/server.py | 39 ++-- .../amazon_keyspaces_mcp_server/services.py | 3 +- .../tests/test_client.py | 11 +- .../tests/test_llm_context.py | 11 +- .../tests/test_main.py | 3 +- .../tests/test_models.py | 6 +- .../tests/test_query_analysis_service.py | 7 +- .../tests/test_server.py | 11 +- .../tests/test_services.py | 4 +- 15 files changed, 160 insertions(+), 220 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/README.md b/src/amazon-keyspaces-mcp-server/README.md index 2230cf567f..b86af1abb7 100644 --- a/src/amazon-keyspaces-mcp-server/README.md +++ b/src/amazon-keyspaces-mcp-server/README.md @@ -146,7 +146,7 @@ Edit the Kiro configuration file at `~/.kiro/settings/mcp.json`: "autoApprove" : [] } } - + } ``` diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py index b459856375..114ac1d691 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/client.py @@ -21,11 +21,6 @@ import logging import os import ssl -from typing import Any, Dict, List, Optional - -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, Session - from .consts import ( CERT_DIRECTORY, CERT_FILENAME, @@ -35,6 +30,9 @@ PROTOCOL_VERSION, UNSAFE_OPERATIONS, ) +from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster, Session +from typing import Any, Dict, List, Optional # Older versions of the Cassandra Python driver may not include SSLOptions. Conditionally @@ -252,8 +250,7 @@ async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, """Get detailed information about a table.""" try: query = ( - 'SELECT * FROM system_schema.tables WHERE ' - 'keyspace_name = %s AND table_name = %s' + 'SELECT * FROM system_schema.tables WHERE keyspace_name = %s AND table_name = %s' ) session = await self.get_session() @@ -318,9 +315,7 @@ async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, ) session = await self.get_session() - capacity_row = session.execute( - query, [keyspace_name, table_name] - ).one() + capacity_row = session.execute(query, [keyspace_name, table_name]).one() if capacity_row and capacity_row.custom_properties: props = capacity_row.custom_properties @@ -328,13 +323,20 @@ async def describe_table(self, keyspace_name: str, table_name: str) -> Dict[str, table_details['capacity_mode'] = props['capacity_mode'] if props['capacity_mode'] == 'PROVISIONED': - if 'read_capacity_units' not in props or 'write_capacity_units' not in props: + if ( + 'read_capacity_units' not in props + or 'write_capacity_units' not in props + ): raise RuntimeError( f'PROVISIONED capacity mode requires both read_capacity_units ' f'and write_capacity_units for table {keyspace_name}.{table_name}' ) - table_details['read_capacity_units'] = int(props['read_capacity_units']) - table_details['write_capacity_units'] = int(props['write_capacity_units']) + table_details['read_capacity_units'] = int( + props['read_capacity_units'] + ) + table_details['write_capacity_units'] = int( + props['write_capacity_units'] + ) except (RuntimeError, ValueError, AttributeError) as e: # Ignore errors when trying to get capacity information logger.warning( @@ -361,10 +363,7 @@ async def execute_read_only_query( raise ValueError('Only SELECT queries are allowed for read-only execution') # Check for any modifications that might be disguised as SELECT - if any( - op in trimmed_query - for op in UNSAFE_OPERATIONS - ): + if any(op in trimmed_query for op in UNSAFE_OPERATIONS): raise ValueError('Query contains potentially unsafe operations') try: @@ -398,9 +397,7 @@ async def execute_read_only_query( if hasattr(row, col_name) and getattr(row, col_name) is not None: value = getattr(row, col_name) except (AttributeError, TypeError, ValueError) as e: - logger.warning( - 'Error getting value for column %s: %s', col_name, str(e) - ) + logger.warning('Error getting value for column %s: %s', col_name, str(e)) row_data[col_name] = value rows.append(row_data) diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py index 7b39fc3d4e..09f5581677 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/config.py @@ -14,11 +14,10 @@ """Configuration module for Keyspaces MCP Server.""" import os +from .consts import CASSANDRA_DEFAULT_PORT, ENV_DIRECTORY, ENV_FILENAME from dataclasses import dataclass from dotenv import load_dotenv -from .consts import CASSANDRA_DEFAULT_PORT, ENV_DIRECTORY, ENV_FILENAME - # Load environment variables from ENV_FILENAME in the user's home directory, # if it exists. diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py index ecc06d9550..822dd9357c 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/exceptions.py @@ -17,7 +17,7 @@ class KeyspacesException(Exception): """Base exception for Keyspaces MCP server errors.""" - def __init__(self, message: str, details: str = ""): + def __init__(self, message: str, details: str = ''): """Initialize exception with message and optional details.""" self.message = message self.details = details diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py index 097274e786..5c7acbc8c6 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/llm_context.py @@ -13,9 +13,8 @@ # limitations under the License. """LLM context builder for Keyspaces MCP Server.""" -from typing import Any, Dict, List - from .models import KeyspaceInfo, QueryAnalysisResult, TableInfo +from typing import Any, Dict, List def build_list_keyspaces_context(_keyspaces: List[KeyspaceInfo]) -> str: @@ -27,26 +26,25 @@ def build_list_keyspaces_context(_keyspaces: List[KeyspaceInfo]) -> str: # Add keyspace-specific guidance list_keyspaces_guidance = { - "compatibility": ( - "Amazon Keyspaces is compatible with Apache Cassandra 3.11. " - "This means that it supports most of the same CQL language features " - "and is driver-protocol compatible with Cassandra 3.11." + 'compatibility': ( + 'Amazon Keyspaces is compatible with Apache Cassandra 3.11. ' + 'This means that it supports most of the same CQL language features ' + 'and is driver-protocol compatible with Cassandra 3.11.' ), - "limitations": ( + 'limitations': ( "Amazon Keyspaces doesn't support all Apache Cassandra 3.11 features. " - "For the most current list of unsupported features and CQL limitations, " - "refer to the CQL Reference resource (keyspaces://cql-reference) which " - "includes up-to-date information on supported operations, data types, " - "and known restrictions." + 'For the most current list of unsupported features and CQL limitations, ' + 'refer to the CQL Reference resource (keyspaces://cql-reference) which ' + 'includes up-to-date information on supported operations, data types, ' + 'and known restrictions.' ), - "replication_strategy": ( - "In Cassandra, common replication strategies include SimpleStrategy and " - "NetworkTopologyStrategy. Amazon Keyspaces uses a single-region " - "replication strategy with 3x replication for durability." + 'replication_strategy': ( + 'In Cassandra, common replication strategies include SimpleStrategy and ' + 'NetworkTopologyStrategy. Amazon Keyspaces uses a single-region ' + 'replication strategy with 3x replication for durability.' ), - "naming_conventions": ( - "Keyspace names typically use snake_case and represent logical data " - "domains." + 'naming_conventions': ( + 'Keyspace names typically use snake_case and represent logical data domains.' ), } context['list_keyspaces_guidance'] = list_keyspaces_guidance @@ -63,23 +61,23 @@ def build_list_tables_context(_keyspace_name: str, _tables: List[TableInfo]) -> # Add table-specific guidance tables_guidance = { - "data_modeling": ( - "For detailed information on Cassandra data modeling, primary keys, " - "partition keys, and clustering columns, refer to the Developer Guide " - "resource (keyspaces://developer-guide)." + 'data_modeling': ( + 'For detailed information on Cassandra data modeling, primary keys, ' + 'partition keys, and clustering columns, refer to the Developer Guide ' + 'resource (keyspaces://developer-guide).' ), - "naming_conventions": ( - "Table names typically use snake_case and should be descriptive of the " - "entity they represent." + 'naming_conventions': ( + 'Table names typically use snake_case and should be descriptive of the ' + 'entity they represent.' ), } # Add guidance for empty results if not _tables: - tables_guidance["empty_result_interpretation"] = ( - "When no tables are found, this could mean either: (1) the keyspace exists " - "but contains no tables, or (2) the keyspace does not exist. If you suspect " - "the keyspace might not exist, use the listKeyspaces tool to verify the " + tables_guidance['empty_result_interpretation'] = ( + 'When no tables are found, this could mean either: (1) the keyspace exists ' + 'but contains no tables, or (2) the keyspace does not exist. If you suspect ' + 'the keyspace might not exist, use the listKeyspaces tool to verify the ' "keyspace name is correct before concluding it's empty." ) @@ -97,14 +95,14 @@ def build_keyspace_details_context(_keyspace_details: Dict[str, Any]) -> str: # Add keyspace-specific guidance keyspace_guidance = { - "replication_strategy": ( - "Replication strategy determines how data is distributed across nodes. " - "Amazon Keyspaces manages replication automatically for high availability." + 'replication_strategy': ( + 'Replication strategy determines how data is distributed across nodes. ' + 'Amazon Keyspaces manages replication automatically for high availability.' ), - "durable_writes": ( - "Durable writes ensure data is written to the commit log before " - "acknowledging the write. This provides durability in case of node " - "failures." + 'durable_writes': ( + 'Durable writes ensure data is written to the commit log before ' + 'acknowledging the write. This provides durability in case of node ' + 'failures.' ), } context['keyspace_guidance'] = keyspace_guidance @@ -131,18 +129,18 @@ def build_table_details_context(table_details: Dict[str, Any]) -> str: # Add table-specific guidance table_guidance = { - "partition_key": ( - "Partition keys determine data distribution across the cluster. " - "Queries are most efficient when they include the partition key." + 'partition_key': ( + 'Partition keys determine data distribution across the cluster. ' + 'Queries are most efficient when they include the partition key.' ), - "clustering_columns": ( - "Clustering columns determine the sort order within a partition. " - "They enable range queries within a partition." + 'clustering_columns': ( + 'Clustering columns determine the sort order within a partition. ' + 'They enable range queries within a partition.' ), - "secondary_indexes": ( - "Secondary indexes should be used sparingly in Cassandra. " - "They are best for low-cardinality columns and can impact write " - "performance." + 'secondary_indexes': ( + 'Secondary indexes should be used sparingly in Cassandra. ' + 'They are best for low-cardinality columns and can impact write ' + 'performance.' ), } context['table_guidance'] = table_guidance @@ -159,19 +157,19 @@ def build_query_result_context(query_results: Dict[str, Any]) -> str: # Add query-specific guidance query_guidance = { - "performance_considerations": ( - "Cassandra queries are most efficient when they include the partition key. " - "Queries without a partition key may require a full table scan, which can " - "be inefficient for large tables." + 'performance_considerations': ( + 'Cassandra queries are most efficient when they include the partition key. ' + 'Queries without a partition key may require a full table scan, which can ' + 'be inefficient for large tables.' ), - "pagination": ( - "For large result sets, consider using pagination with the LIMIT clause " - "and token-based paging to avoid loading too many rows in memory." + 'pagination': ( + 'For large result sets, consider using pagination with the LIMIT clause ' + 'and token-based paging to avoid loading too many rows in memory.' ), - "consistency_level": ( - "The consistency level determines how many replicas must acknowledge a " - "read request before returning data. Higher consistency levels provide " - "stronger guarantees but may increase latency." + 'consistency_level': ( + 'The consistency level determines how many replicas must acknowledge a ' + 'read request before returning data. Higher consistency levels provide ' + 'stronger guarantees but may increase latency.' ), } @@ -181,14 +179,14 @@ def build_query_result_context(query_results: Dict[str, Any]) -> str: result_guidance = {} if row_count == 0: - result_guidance["empty_result"] = ( - "No rows were returned. This could mean either no matching data exists " - "or the query conditions were too restrictive." + result_guidance['empty_result'] = ( + 'No rows were returned. This could mean either no matching data exists ' + 'or the query conditions were too restrictive.' ) elif row_count > 100: - result_guidance["large_result"] = ( - "A large number of rows were returned. Consider adding more specific " - "filtering conditions or using pagination for better performance." + result_guidance['large_result'] = ( + 'A large number of rows were returned. Consider adding more specific ' + 'filtering conditions or using pagination for better performance.' ) context['result_guidance'] = result_guidance @@ -205,28 +203,28 @@ def build_query_analysis_context(analysis_result: QueryAnalysisResult) -> str: # Add query performance guidance performance_guidance = { - "partition_key_importance": ( + 'partition_key_importance': ( "In Cassandra/Keyspaces, queries that don't filter on partition key " - "require scanning all partitions, which is extremely expensive and should " - "be avoided." + 'require scanning all partitions, which is extremely expensive and should ' + 'be avoided.' ), - "clustering_column_usage": ( - "After partition keys, clustering columns should be used in WHERE clauses " - "to further narrow down the data that needs to be read within a partition." + 'clustering_column_usage': ( + 'After partition keys, clustering columns should be used in WHERE clauses ' + 'to further narrow down the data that needs to be read within a partition.' ), - "allow_filtering_warning": ( - "The ALLOW FILTERING clause forces Cassandra to scan potentially all " - "partitions, which is very inefficient and should be avoided in production." + 'allow_filtering_warning': ( + 'The ALLOW FILTERING clause forces Cassandra to scan potentially all ' + 'partitions, which is very inefficient and should be avoided in production.' ), - "secondary_indexes": ( - "Secondary indexes in Cassandra are not as efficient as in relational " - "databases. They still require reading from multiple partitions and should " - "be used sparingly." + 'secondary_indexes': ( + 'Secondary indexes in Cassandra are not as efficient as in relational ' + 'databases. They still require reading from multiple partitions and should ' + 'be used sparingly.' ), - "full_table_scan": ( - "Full table scans in Cassandra are extremely expensive operations that " - "should be avoided. Always design your data model and queries to avoid " - "scanning entire tables." + 'full_table_scan': ( + 'Full table scans in Cassandra are extremely expensive operations that ' + 'should be avoided. Always design your data model and queries to avoid ' + 'scanning entire tables.' ), } @@ -249,22 +247,22 @@ def build_query_analysis_context(analysis_result: QueryAnalysisResult) -> str: def build_cassandra_knowledge() -> Dict[str, str]: """Provide general Cassandra knowledge.""" knowledge = { - "data_model": ( - "Cassandra uses a wide-column store data model optimized for write " - "performance and horizontal scalability." + 'data_model': ( + 'Cassandra uses a wide-column store data model optimized for write ' + 'performance and horizontal scalability.' ), - "query_patterns": ( - "Cassandra is optimized for high write throughput and queries that " - "specify the partition key." + 'query_patterns': ( + 'Cassandra is optimized for high write throughput and queries that ' + 'specify the partition key.' ), - "limitations": ( - "Cassandra has limited support for joins, aggregations, and transactions. " - "Data modeling should denormalize data to support specific query patterns." + 'limitations': ( + 'Cassandra has limited support for joins, aggregations, and transactions. ' + 'Data modeling should denormalize data to support specific query patterns.' ), - "keyspaces_vs_cassandra": ( - "Amazon Keyspaces is a managed Cassandra-compatible service with some " - "differences in performance characteristics and feature support compared " - "to self-managed Cassandra." + 'keyspaces_vs_cassandra': ( + 'Amazon Keyspaces is a managed Cassandra-compatible service with some ' + 'differences in performance characteristics and feature support compared ' + 'to self-managed Cassandra.' ), } @@ -274,17 +272,17 @@ def build_cassandra_knowledge() -> Dict[str, str]: def build_amazon_keyspaces_knowledge() -> Dict[str, str]: """Provide Amazon Keyspaces specific knowledge.""" knowledge = { - "compatibility": ( - "For current compatibility information with Apache Cassandra, refer to " - "the Developer Guide resource (keyspaces://developer-guide)." + 'compatibility': ( + 'For current compatibility information with Apache Cassandra, refer to ' + 'the Developer Guide resource (keyspaces://developer-guide).' ), - "differences_from_cassandra": ( - "For Apache Cassandra features and capabilities, refer to the official " - "Apache Cassandra documentation at https://cassandra.apache.org/doc/. " - "For differences between Amazon Keyspaces and Apache Cassandra, including " - "unsupported features and operational differences, refer to the CQL " - "Reference resource (keyspaces://cql-reference) and Developer Guide " - "resource (keyspaces://developer-guide)." + 'differences_from_cassandra': ( + 'For Apache Cassandra features and capabilities, refer to the official ' + 'Apache Cassandra documentation at https://cassandra.apache.org/doc/. ' + 'For differences between Amazon Keyspaces and Apache Cassandra, including ' + 'unsupported features and operational differences, refer to the CQL ' + 'Reference resource (keyspaces://cql-reference) and Developer Guide ' + 'resource (keyspaces://developer-guide).' ), } diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py index cad4eb1d36..6d231126cc 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/models.py @@ -15,9 +15,8 @@ import re from dataclasses import dataclass, field -from typing import Any, Dict, List - from pydantic import BaseModel, Field, field_validator +from typing import Any, Dict, List @dataclass @@ -134,39 +133,27 @@ class QueryAnalysisResult: # Pydantic models for input validation class KeyspaceInput(BaseModel): """Validated keyspace input.""" + keyspace: str = Field( ..., min_length=1, max_length=48, pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$', - description='Keyspace name (alphanumeric and underscore only)' + description='Keyspace name (alphanumeric and underscore only)', ) class TableInput(BaseModel): """Validated table input.""" - keyspace: str = Field( - ..., - min_length=1, - max_length=48, - pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' - ) - table: str = Field( - ..., - min_length=1, - max_length=48, - pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' - ) + + keyspace: str = Field(..., min_length=1, max_length=48, pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$') + table: str = Field(..., min_length=1, max_length=48, pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$') class QueryInput(BaseModel): """Validated query input.""" - keyspace: str = Field( - ..., - min_length=1, - max_length=48, - pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$' - ) + + keyspace: str = Field(..., min_length=1, max_length=48, pattern=r'^[a-zA-Z][a-zA-Z0-9_]*$') query: str = Field(..., min_length=1, max_length=10000) @field_validator('query') @@ -179,11 +166,7 @@ def sanitize_query(cls, v: str) -> str: class ResourceArnInput(BaseModel): """Validated resource ARN input.""" + resource_arn: str = Field( - ..., - min_length=1, - max_length=1000, - description='ARN of the resource (keyspace or table)' + ..., min_length=1, max_length=1000, description='ARN of the resource (keyspace or table)' ) - - diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py index 944bca0157..1be80ad7d5 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/server.py @@ -14,11 +14,6 @@ """awslabs MCP Server implementation for Amazon Keyspaces (for Apache Cassandra).""" import sys -from typing import Any, Optional - -from fastmcp import Context, FastMCP -from loguru import logger - from .client import UnifiedCassandraClient from .config import AppConfig from .consts import ( @@ -43,6 +38,9 @@ ) from .models import KeyspaceInput, QueryInput, TableInput from .services import DataService, QueryAnalysisService, SchemaService +from fastmcp import Context, FastMCP +from loguru import logger +from typing import Any, Optional # Remove all default handlers then add our own @@ -175,7 +173,7 @@ def get_api_reference() -> str: ### Keyspace Management - CreateKeyspace, DeleteKeyspace, GetKeyspace, ListKeyspaces -### Table Management +### Table Management - CreateTable, DeleteTable, GetTable, UpdateTable, ListTables, RestoreTable ### Configuration @@ -463,9 +461,7 @@ async def _handle_list_keyspaces(self, ctx: Optional[Any] = None) -> str: # Add contextual information about Cassandra/Keyspaces if ctx: - ctx.info( - 'Adding contextual information about Cassandra/Keyspaces' - ) # type: ignore[unused-coroutine] + ctx.info('Adding contextual information about Cassandra/Keyspaces') # type: ignore[unused-coroutine] formatted_text += build_list_keyspaces_context(keyspaces) return formatted_text @@ -492,9 +488,7 @@ async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None # Add contextual information about tables in Cassandra if ctx: - ctx.info( - f'Adding contextual information about tables in keyspace {keyspace}' - ) # type: ignore[unused-coroutine] + ctx.info(f'Adding contextual information about tables in keyspace {keyspace}') # type: ignore[unused-coroutine] formatted_text += build_list_tables_context(keyspace, tables) return formatted_text @@ -502,9 +496,7 @@ async def _handle_list_tables(self, keyspace: str, ctx: Optional[Context] = None logger.error(f'Error listing tables: {str(e)}') raise SchemaError('Unable to retrieve table information') from e - async def _handle_describe_keyspace( - self, keyspace: str, ctx: Optional[Context] = None - ) -> str: + async def _handle_describe_keyspace(self, keyspace: str, ctx: Optional[Context] = None) -> str: """Handle the describeKeyspace tool.""" try: if not keyspace: @@ -522,7 +514,7 @@ async def _handle_describe_keyspace( # Add replication factor or datacenter details if 'SimpleStrategy' in replication.get('class', ''): - rf = replication.get("replication_factor", "Unknown") + rf = replication.get('replication_factor', 'Unknown') formatted_text += f'- **Replication Factor**: `{rf}`\n' elif 'NetworkTopologyStrategy' in replication.get('class', ''): formatted_text += '- **Datacenter Replication**:\n' @@ -536,9 +528,7 @@ async def _handle_describe_keyspace( # Add contextual information about replication strategies if ctx: - ctx.info( - 'Adding contextual information about replication strategies' - ) # type: ignore[unused-coroutine] + ctx.info('Adding contextual information about replication strategies') # type: ignore[unused-coroutine] formatted_text += build_keyspace_details_context(keyspace_details) return formatted_text @@ -605,8 +595,7 @@ async def _handle_describe_table( # Add contextual information about Cassandra data types and primary keys if ctx: ctx.info( - 'Adding contextual information about Cassandra data types and ' - 'primary keys' + 'Adding contextual information about Cassandra data types and primary keys' ) # type: ignore[unused-coroutine] formatted_text += build_table_details_context(table_details) @@ -678,9 +667,7 @@ async def _handle_execute_query( # Add contextual information about CQL queries if ctx: - ctx.info( - 'Adding contextual information about CQL queries' - ) # type: ignore[unused-coroutine] + ctx.info('Adding contextual information about CQL queries') # type: ignore[unused-coroutine] formatted_text += build_query_result_context(query_results) return formatted_text @@ -717,9 +704,7 @@ async def _handle_analyze_query_performance( # Add contextual information about query performance in Cassandra if ctx: - ctx.info( - 'Adding contextual information about query performance in Cassandra' - ) # type: ignore[unused-coroutine] + ctx.info('Adding contextual information about query performance in Cassandra') # type: ignore[unused-coroutine] formatted_text += build_query_analysis_context(analysis_result) return formatted_text diff --git a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py index c53f6f3ab8..648c51bf95 100644 --- a/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py +++ b/src/amazon-keyspaces-mcp-server/awslabs/amazon_keyspaces_mcp_server/services.py @@ -15,10 +15,9 @@ import logging import re -from typing import Any, Dict, List - from .client import UnifiedCassandraClient from .models import KeyspaceInfo, QueryAnalysisResult, TableInfo +from typing import Any, Dict, List logger = logging.getLogger(__name__) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_client.py b/src/amazon-keyspaces-mcp-server/tests/test_client.py index ae54bc714f..264767769f 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_client.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_client.py @@ -15,11 +15,6 @@ import ssl import unittest -from unittest.mock import Mock, patch - -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, Session - from awslabs.amazon_keyspaces_mcp_server.client import UnifiedCassandraClient from awslabs.amazon_keyspaces_mcp_server.config import DatabaseConfig from awslabs.amazon_keyspaces_mcp_server.consts import ( @@ -27,6 +22,9 @@ KEYSPACES_DEFAULT_PORT, ) from awslabs.amazon_keyspaces_mcp_server.models import TableInfo +from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster, Session +from unittest.mock import Mock, patch # pylint: disable=no-member @@ -694,7 +692,7 @@ async def test_describe_table_with_capacity_mode(self): mock_capacity_row.custom_properties = { 'capacity_mode': 'PROVISIONED', 'read_capacity_units': '100', - 'write_capacity_units': '50' + 'write_capacity_units': '50', } def mock_execute(query, _params=None): @@ -788,7 +786,6 @@ def mock_execute(query, _params=None): self.assertNotIn('read_capacity_units', table_details) self.assertNotIn('write_capacity_units', table_details) - async def test_describe_table_with_on_demand_capacity(self): """Test table with ON_DEMAND capacity mode.""" mock_table_row = Mock() diff --git a/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py b/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py index 82a32d6e35..df1f2ee726 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_llm_context.py @@ -15,7 +15,6 @@ """Tests for llm_context module.""" import unittest - from awslabs.amazon_keyspaces_mcp_server.llm_context import ( build_query_result_context, build_table_details_context, @@ -31,9 +30,7 @@ def test_build_table_details_context_with_keyspaces_context(self): table_details = { 'keyspace_name': 'test_ks', 'table_name': 'test_table', - '_keyspaces_context': { - 'service_characteristics': 'serverless' - } + '_keyspaces_context': {'service_characteristics': 'serverless'}, } result = build_table_details_context(table_details) self.assertIn('Service Characteristics', result) @@ -41,11 +38,7 @@ def test_build_table_details_context_with_keyspaces_context(self): def test_build_query_result_context_large_result(self): """Test query result context with large result set.""" - query_results = { - 'row_count': 150, - 'columns': ['id'], - 'rows': [] - } + query_results = {'row_count': 150, 'columns': ['id'], 'rows': []} result = build_query_result_context(query_results) self.assertIn('Large Result', result) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_main.py b/src/amazon-keyspaces-mcp-server/tests/test_main.py index 2f2aaf0772..4c920a2c6c 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_main.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_main.py @@ -14,9 +14,8 @@ """Tests for the main function in server.py.""" import inspect -from unittest.mock import patch - from awslabs.amazon_keyspaces_mcp_server.server import main +from unittest.mock import patch class TestMain: diff --git a/src/amazon-keyspaces-mcp-server/tests/test_models.py b/src/amazon-keyspaces-mcp-server/tests/test_models.py index 0e01439279..fe5c0f25c1 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_models.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_models.py @@ -15,7 +15,6 @@ """Tests for models module.""" import unittest - from awslabs.amazon_keyspaces_mcp_server.models import QueryInput @@ -24,10 +23,7 @@ class TestModels(unittest.TestCase): def test_sanitize_query_with_hidden_characters(self): """Test query sanitization removes hidden unicode characters.""" - input_data = QueryInput( - keyspace='test', - query='SELECT\u200B * FROM\uFEFF users\u0000' - ) + input_data = QueryInput(keyspace='test', query='SELECT\u200b * FROM\ufeff users\u0000') self.assertEqual(input_data.query, 'SELECT * FROM users') diff --git a/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py b/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py index 856934eaf7..78f1cf6d5f 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_query_analysis_service.py @@ -14,13 +14,12 @@ """Unit tests for the QueryAnalysisService class.""" import unittest -from unittest.mock import AsyncMock, Mock, PropertyMock - from awslabs.amazon_keyspaces_mcp_server.models import QueryAnalysisResult from awslabs.amazon_keyspaces_mcp_server.services import ( QueryAnalysisService, SchemaService, ) +from unittest.mock import AsyncMock, Mock, PropertyMock # pylint: disable=protected-access @@ -341,9 +340,7 @@ async def test_analyze_query_unable_to_determine_table(self): async def test_extract_where_conditions_no_where(self): """Test extracting WHERE conditions from query without WHERE clause.""" - conditions = self.query_analysis_service._extract_where_conditions( - 'SELECT * FROM users' - ) + conditions = self.query_analysis_service._extract_where_conditions('SELECT * FROM users') self.assertEqual(conditions, []) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_server.py b/src/amazon-keyspaces-mcp-server/tests/test_server.py index b11a54250e..d83de8baf2 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_server.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_server.py @@ -13,11 +13,8 @@ # and limitations under the License. """Unit tests for the server module.""" -import unittest -from unittest.mock import AsyncMock, Mock, patch - import pytest - +import unittest from awslabs.amazon_keyspaces_mcp_server.consts import MAX_DISPLAY_ROWS from awslabs.amazon_keyspaces_mcp_server.models import ( KeyspaceInfo, @@ -29,6 +26,7 @@ get_proxy, ) from mcp.server.fastmcp import Context +from unittest.mock import AsyncMock, Mock, patch # pylint: disable=protected-access,too-many-public-methods @@ -540,6 +538,7 @@ async def test_get_proxy(mock_app_config, mock_client_class): # Reset the global proxy # pylint: disable=import-outside-toplevel import awslabs.amazon_keyspaces_mcp_server.server as server_module + server_module._PROXY = None # pylint: disable=protected-access # Set up the mocks @@ -564,7 +563,7 @@ async def test_get_proxy(mock_app_config, mock_client_class): if __name__ == '__main__': import sys - print('\n' + '='*70) + print('\n' + '=' * 70) print('Running Amazon Keyspaces MCP Server Tests') - print('='*70 + '\n') + print('=' * 70 + '\n') sys.exit(pytest.main([__file__, '-v', '--tb=short'])) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_services.py b/src/amazon-keyspaces-mcp-server/tests/test_services.py index 6085ea84e7..b975daa180 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_services.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_services.py @@ -14,10 +14,9 @@ """Unit tests for the services module.""" import unittest -from unittest.mock import AsyncMock, Mock - from awslabs.amazon_keyspaces_mcp_server.models import KeyspaceInfo, TableInfo from awslabs.amazon_keyspaces_mcp_server.services import DataService, SchemaService +from unittest.mock import AsyncMock, Mock class TestDataService(unittest.IsolatedAsyncioTestCase): @@ -152,4 +151,3 @@ async def test_describe_table(self): if __name__ == '__main__': unittest.main() - From e1d278abd8051b4ac1a38b3e47d784154c87c92a Mon Sep 17 00:00:00 2001 From: Michael Christensen <32532449+im-michaelc@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:39:01 -0800 Subject: [PATCH 23/24] Update CodeQL workflow permissions Added permissions for security events and actions. --- .github/workflows/codeql.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a61dde32e0..a1efb28e4f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,7 +18,12 @@ on: branches: [ "main" ] schedule: - cron: '20 8 * * 3' -permissions: {} +permissions: + security-events: write + actions: read + contents: read + packages: read + pull-requests: read jobs: analyze: name: Analyze (${{ matrix.language }}) From 8f741fb6d0951442d9457016c77ac10b31343d2d Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Tue, 9 Dec 2025 10:47:02 -0800 Subject: [PATCH 24/24] fix: mock database connection in tests and fix CodeQL permissions --- src/amazon-keyspaces-mcp-server/tests/test_main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/amazon-keyspaces-mcp-server/tests/test_main.py b/src/amazon-keyspaces-mcp-server/tests/test_main.py index 4c920a2c6c..4d50f796bf 100644 --- a/src/amazon-keyspaces-mcp-server/tests/test_main.py +++ b/src/amazon-keyspaces-mcp-server/tests/test_main.py @@ -15,15 +15,16 @@ import inspect from awslabs.amazon_keyspaces_mcp_server.server import main -from unittest.mock import patch +from unittest.mock import AsyncMock, patch class TestMain: """Tests for the main function.""" @patch('awslabs.amazon_keyspaces_mcp_server.server.mcp.run') + @patch('awslabs.amazon_keyspaces_mcp_server.client.UnifiedCassandraClient.get_session') @patch('sys.argv', ['awslabs.amazon-keyspaces-mcp-server']) - def test_main_default(self, mock_run): + def test_main_default(self, mock_get_session, mock_run): """Test main function with default arguments. This test verifies that: @@ -31,6 +32,7 @@ def test_main_default(self, mock_run): 2. The mcp.run method is called once 3. No transport parameter is passed to mcp.run """ + mock_get_session.return_value = AsyncMock() main() mock_run.assert_called_once() assert mock_run.call_args[1].get('transport') is None