From 47c7052c860dbf66c98f7658250ce6fbed556f7f Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Thu, 5 Mar 2026 12:21:50 -0600 Subject: [PATCH 01/29] feat(parser): add Perl language support - Add PERL_SPEC to languages.py with subroutine_declaration_statement (function) and package_statement (class) node types - Register .pl, .pm, .t extensions in LANGUAGE_EXTENSIONS - Add "pod" to _extract_preceding_comments() for POD docstring support - Add use_statement branch to _extract_constant() for Perl constants - Create tests/fixtures/perl/sample.pl fixture file - Add test_parse_perl() covering packages, subs, comments, constants - Add "perl" to search_symbols language enum in server.py Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/parser/extractor.py | 32 +++++++++++++++- src/jcodemunch_mcp/parser/languages.py | 25 ++++++++++++ src/jcodemunch_mcp/server.py | 2 +- tests/fixtures/perl/sample.pl | 47 +++++++++++++++++++++++ tests/test_languages.py | 53 ++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/perl/sample.pl diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 707bfa4..751f45b 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -280,7 +280,7 @@ def _extract_preceding_comments(node, source_bytes: bytes) -> str: prev = node.prev_named_sibling while prev and prev.type in ("annotation", "marker_annotation"): prev = prev.prev_named_sibling - while prev and prev.type in ("comment", "line_comment", "block_comment", "documentation_comment"): + while prev and prev.type in ("comment", "line_comment", "block_comment", "documentation_comment", "pod"): comment_text = source_bytes[prev.start_byte:prev.end_byte].decode("utf-8") comments.insert(0, comment_text) prev = prev.prev_named_sibling @@ -402,6 +402,36 @@ def _extract_constant( content_hash=c_hash, ) + # Perl: use constant NAME => value + if node.type == "use_statement": + children = list(node.children) + if len(children) >= 3 and children[1].type == "package": + pkg_name = source_bytes[children[1].start_byte:children[1].end_byte].decode("utf-8") + if pkg_name == "constant": + for child in children: + if child.type == "list_expression" and child.child_count >= 1: + name_node = child.children[0] + if name_node.type == "autoquoted_bareword": + name = source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8") + if name.isupper() or (len(name) > 1 and name[0].isupper()): + sig = source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() + const_bytes = source_bytes[node.start_byte:node.end_byte] + c_hash = compute_content_hash(const_bytes) + return Symbol( + id=make_symbol_id(filename, name, "constant"), + file=filename, + name=name, + qualified_name=name, + kind="constant", + language=language, + signature=sig[:100], + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=c_hash, + ) + return None diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 2c9b8de..c8c49ff 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -62,6 +62,9 @@ class LanguageSpec: ".cs": "csharp", ".c": "c", ".h": "c", + ".pl": "perl", + ".pm": "perl", + ".t": "perl", } @@ -387,6 +390,27 @@ class LanguageSpec: ) +# Perl specification +PERL_SPEC = LanguageSpec( + ts_language="perl", + symbol_node_types={ + "subroutine_declaration_statement": "function", + "package_statement": "class", + }, + name_fields={ + "subroutine_declaration_statement": "name", + "package_statement": "name", + }, + param_fields={}, + return_type_fields={}, + docstring_strategy="preceding_comment", + decorator_node_type=None, + container_node_types=[], + constant_patterns=["use_statement"], + type_patterns=[], +) + + # Language registry LANGUAGE_REGISTRY = { "python": PYTHON_SPEC, @@ -399,4 +423,5 @@ class LanguageSpec: "dart": DART_SPEC, "csharp": CSHARP_SPEC, "c": C_SPEC, + "perl": PERL_SPEC, } diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 69da8d7..efc42d3 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -212,7 +212,7 @@ async def list_tools() -> list[Tool]: "language": { "type": "string", "description": "Optional filter by language", - "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c"] + "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "perl"] }, "max_results": { "type": "integer", diff --git a/tests/fixtures/perl/sample.pl b/tests/fixtures/perl/sample.pl new file mode 100644 index 0000000..c1505da --- /dev/null +++ b/tests/fixtures/perl/sample.pl @@ -0,0 +1,47 @@ +#!/usr/bin/perl +use strict; +use warnings; + +package Animal; + +use constant MAX_LEGS => 4; +use constant KINGDOM => 'Animalia'; + +# Create a new Animal +sub new { + my ($class, %args) = @_; + return bless { name => $args{name}, legs => $args{legs} // 4 }, $class; +} + +# Get the animal's name +sub get_name { + my ($self) = @_; + return $self->{name}; +} + +=pod + +=head1 describe + +Returns a description of the animal. + +=cut + +sub describe { + my ($self) = @_; + return "Animal: " . $self->{name}; +} + +package Animal::Dog; + +sub new { + my ($class, %args) = @_; + return bless { name => $args{name} }, $class; +} + +sub bark { + my ($self) = @_; + return "Woof!"; +} + +1; diff --git a/tests/test_languages.py b/tests/test_languages.py index a1d606f..aff8520 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -519,3 +519,56 @@ def test_parse_c(): assert const.kind == "constant" +PERL_SOURCE = ''' +package Animal; + +use constant MAX_LEGS => 4; + +# Create a new Animal +sub new { + my ($class, %args) = @_; + return bless {}, $class; +} + +# Get the animal name +sub get_name { + my ($self) = @_; + return $self->{name}; +} + +package Animal::Dog; + +sub bark { + my ($self) = @_; + return "Woof!"; +} + +1; +''' + + +def test_parse_perl(): + """Test Perl parsing.""" + symbols = parse_file(PERL_SOURCE, "animal.pl", "perl") + + # Should have packages (class), subroutines (function), constants + animal_pkg = next((s for s in symbols if s.name == "Animal"), None) + assert animal_pkg is not None + assert animal_pkg.kind == "class" + + new_sub = next((s for s in symbols if s.name == "new"), None) + assert new_sub is not None + assert new_sub.kind == "function" + assert "Create a new Animal" in new_sub.docstring + + get_name_sub = next((s for s in symbols if s.name == "get_name"), None) + assert get_name_sub is not None + assert get_name_sub.kind == "function" + + bark_sub = next((s for s in symbols if s.name == "bark"), None) + assert bark_sub is not None + assert bark_sub.kind == "function" + + max_legs = next((s for s in symbols if s.name == "MAX_LEGS"), None) + assert max_legs is not None + assert max_legs.kind == "constant" From 67f1f470eb99394869e296632125593163dd8008 Mon Sep 17 00:00:00 2001 From: "J. Gravelle" Date: Thu, 5 Mar 2026 15:26:57 -0600 Subject: [PATCH 02/29] Add Swift language support (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Swift to the language registry using tree-sitter-swift (already bundled in tree-sitter-language-pack — no new dependency). Changes: - languages.py: SWIFT_SPEC covering function_declaration, class_declaration (class/struct/enum/extension), protocol_declaration, init_declaration; preceding_comment docstring strategy for /// and /* */ comments - extractor.py: property_declaration handler in _extract_constant for Swift let MAX_CONST = ... bindings - server.py: adds "swift" to the search_symbols language enum - tests/test_languages.py: test_parse_swift covering function, class, struct, protocol, enum, init, method, and constant extraction Co-authored-by: Claude Sonnet 4.6 --- src/jcodemunch_mcp/parser/extractor.py | 46 +++++++++++++++ src/jcodemunch_mcp/parser/languages.py | 32 ++++++++++ src/jcodemunch_mcp/server.py | 2 +- tests/test_languages.py | 82 ++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 707bfa4..704217e 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -402,6 +402,52 @@ def _extract_constant( content_hash=c_hash, ) + # Swift: let MAX_SPEED = 100 (property_declaration with let binding) + if node.type == "property_declaration": + # Only extract immutable `let` bindings (not `var`) + binding = None + for child in node.children: + if child.type == "value_binding_pattern": + binding = child + break + if not binding: + return None + mutability = binding.child_by_field_name("mutability") + if not mutability or mutability.text != b"let": + return None + pattern = node.child_by_field_name("name") + if not pattern: + return None + name_node = pattern.child_by_field_name("bound_identifier") + if not name_node: + # fallback: first simple_identifier in pattern + for child in pattern.children: + if child.type == "simple_identifier": + name_node = child + break + if not name_node: + return None + name = source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8") + if not (name.isupper() or (len(name) > 1 and name[0].isupper() and "_" in name)): + return None + sig = source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() + const_bytes = source_bytes[node.start_byte:node.end_byte] + c_hash = compute_content_hash(const_bytes) + return Symbol( + id=make_symbol_id(filename, name, "constant"), + file=filename, + name=name, + qualified_name=name, + kind="constant", + language=language, + signature=sig[:100], + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=c_hash, + ) + return None diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 2c9b8de..7e4292f 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -62,6 +62,7 @@ class LanguageSpec: ".cs": "csharp", ".c": "c", ".h": "c", + ".swift": "swift", } @@ -387,6 +388,36 @@ class LanguageSpec: ) +# Swift specification +# Note: tree-sitter-swift uses class_declaration for class/struct/enum/extension; +# the declaration_kind child field ("class"/"struct"/"enum"/"extension") disambiguates +# at the source level but all map to "class" here for uniform treatment. +# Attributes (@discardableResult etc.) live inside a modifiers child node rather +# than as preceding siblings, so decorator extraction is not supported in this spec. +SWIFT_SPEC = LanguageSpec( + ts_language="swift", + symbol_node_types={ + "function_declaration": "function", + "class_declaration": "class", # covers class, struct, enum, extension + "protocol_declaration": "type", + "init_declaration": "method", + }, + name_fields={ + "function_declaration": "name", # simple_identifier child + "class_declaration": "name", # type_identifier child + "protocol_declaration": "name", # type_identifier child + "init_declaration": "name", # "init" keyword token + }, + param_fields={}, # Swift params are unnamed children; signature captured via source range + return_type_fields={}, # return type shares field "name" with function identifier + docstring_strategy="preceding_comment", # /// and /* */ doc comments + decorator_node_type=None, + container_node_types=["class_declaration", "protocol_declaration"], + constant_patterns=["property_declaration"], # let/var at file scope + type_patterns=["protocol_declaration"], +) + + # Language registry LANGUAGE_REGISTRY = { "python": PYTHON_SPEC, @@ -399,4 +430,5 @@ class LanguageSpec: "dart": DART_SPEC, "csharp": CSHARP_SPEC, "c": C_SPEC, + "swift": SWIFT_SPEC, } diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 69da8d7..40e1d9a 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -212,7 +212,7 @@ async def list_tools() -> list[Tool]: "language": { "type": "string", "description": "Optional filter by language", - "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c"] + "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "swift"] }, "max_results": { "type": "integer", diff --git a/tests/test_languages.py b/tests/test_languages.py index a1d606f..b50763b 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -489,6 +489,88 @@ def test_parse_csharp(): assert record.kind == "class" +SWIFT_SOURCE = ''' +/// Greet a user by name. +func greet(name: String) -> String { + return "Hello, \\(name)!" +} + +/// A simple animal. +class Animal { + /// Initialize with a name. + init(name: String) {} + + /// Make the animal speak. + func speak() {} +} + +/// A 2D point. +struct Point { + var x: Double + var y: Double +} + +/// Drawable objects. +protocol Drawable { + func draw() +} + +/// Cardinal directions. +enum Direction { + case north, south, east, west +} + +let MAX_SPEED = 100 +''' + + +def test_parse_swift(): + """Test Swift parsing.""" + symbols = parse_file(SWIFT_SOURCE, "app.swift", "swift") + + # Top-level function + func = next((s for s in symbols if s.name == "greet"), None) + assert func is not None + assert func.kind == "function" + assert "Greet a user by name" in func.docstring + + # Class + cls = next((s for s in symbols if s.name == "Animal"), None) + assert cls is not None + assert cls.kind == "class" + assert "simple animal" in cls.docstring + + # init inside class + init = next((s for s in symbols if s.name == "init"), None) + assert init is not None + assert init.kind == "method" + + # Method inside class + speak = next((s for s in symbols if s.name == "speak"), None) + assert speak is not None + assert speak.kind in ("function", "method") + + # Struct (maps to class) + point = next((s for s in symbols if s.name == "Point"), None) + assert point is not None + assert point.kind == "class" + + # Protocol (maps to type) + drawable = next((s for s in symbols if s.name == "Drawable"), None) + assert drawable is not None + assert drawable.kind == "type" + + # Enum (maps to class via class_declaration) + direction = next((s for s in symbols if s.name == "Direction"), None) + assert direction is not None + assert direction.kind == "class" + + # Constant + speed = next((s for s in symbols if s.name == "MAX_SPEED"), None) + assert speed is not None + assert speed.kind == "constant" + + def test_parse_c(): """Test C parsing.""" symbols = parse_file(C_SOURCE, "sample.c", "c") From ec659c00c83e1aee11b0bd6ee832299c4f62b5a9 Mon Sep 17 00:00:00 2001 From: jgravelle Date: Thu, 5 Mar 2026 15:28:54 -0600 Subject: [PATCH 03/29] =?UTF-8?q?Bump=20version=20to=200.2.14=20=E2=80=94?= =?UTF-8?q?=20Swift=20language=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d048cc2..058104c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.13" +version = "0.2.14" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" From 6e7af7f086c9537fd593d9ee670d4f0b1639690f Mon Sep 17 00:00:00 2001 From: Ivan Milov <522518+ivanmilov@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:53:32 +0100 Subject: [PATCH 04/29] feat: add C++ language support (#24) --- LANGUAGE_SUPPORT.md | 1 + README.md | 1 + src/jcodemunch_mcp/parser/extractor.py | 2 +- src/jcodemunch_mcp/parser/languages.py | 40 +++++++++++ src/jcodemunch_mcp/parser/symbols.py | 2 +- src/jcodemunch_mcp/server.py | 2 +- tests/fixtures/cpp/sample.cpp | 69 +++++++++++++++++++ tests/test_hardening.py | 42 ++++++++++++ tests/test_languages.py | 92 ++++++++++++++++++++++++++ 9 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/cpp/sample.cpp diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index ba1c9b7..cb03212 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -14,6 +14,7 @@ | Dart | `.dart` | tree-sitter-dart | function, class (class/mixin/extension), method, type (enum/typedef) | `@annotation` | `///` doc comments | Constructors and top-level constants are not indexed | | C# | `.cs` | tree-sitter-csharp | class (class/record), method (method/constructor), type (interface/enum/struct/delegate) | `[Attribute]` | `/// ` XML doc comments | Properties and `const` fields not indexed | | C | `.c`, `.h` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | +| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | tree-sitter-cpp | function, class, method, type (struct/enum/union), constant | — | `/* */` and `//` comments | Templates unwrapped to inner declaration; namespace contents extracted as top-level | --- diff --git a/README.md b/README.md index 7cbeff4..823ac30 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,7 @@ Every tool response includes a `_meta` envelope with timing, token savings, and | Dart | `.dart` | function, class, method, type | | C# | `.cs` | class, method, type, record | | C | `.c`, `.h` | function, type, constant | +| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | function, class, method, type, constant | See LANGUAGE_SUPPORT.md for full semantics. diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 704217e..0231dfd 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -190,7 +190,7 @@ def _extract_name(node, spec: LanguageSpec, source_bytes: bytes) -> Optional[str if name_node: # C function_definition: declarator is a function_declarator, # which wraps the actual identifier. Unwrap recursively. - while name_node.type in ("function_declarator", "pointer_declarator"): + while name_node.type in ("function_declarator", "pointer_declarator", "reference_declarator"): inner = name_node.child_by_field_name("declarator") if inner: name_node = inner diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 7e4292f..4b21ede 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -62,6 +62,12 @@ class LanguageSpec: ".cs": "csharp", ".c": "c", ".h": "c", + ".cpp": "cpp", + ".hpp": "cpp", + ".cc": "cpp", + ".hh": "cpp", + ".cxx": "cpp", + ".hxx": "cpp", ".swift": "swift", } @@ -388,6 +394,39 @@ class LanguageSpec: ) +# C++ specification +CPP_SPEC = LanguageSpec( + ts_language="cpp", + symbol_node_types={ + "function_definition": "function", + "class_specifier": "class", + "struct_specifier": "type", + "enum_specifier": "type", + "union_specifier": "type", + "type_definition": "type", + }, + name_fields={ + "function_definition": "declarator", + "class_specifier": "name", + "struct_specifier": "name", + "enum_specifier": "name", + "union_specifier": "name", + "type_definition": "declarator", + }, + param_fields={ + "function_definition": "declarator", + }, + return_type_fields={ + "function_definition": "type", + }, + docstring_strategy="preceding_comment", + decorator_node_type=None, + container_node_types=["class_specifier", "struct_specifier"], + constant_patterns=["preproc_def"], + type_patterns=["type_definition", "enum_specifier", "struct_specifier", "union_specifier"], +) + + # Swift specification # Note: tree-sitter-swift uses class_declaration for class/struct/enum/extension; # the declaration_kind child field ("class"/"struct"/"enum"/"extension") disambiguates @@ -430,5 +469,6 @@ class LanguageSpec: "dart": DART_SPEC, "csharp": CSHARP_SPEC, "c": C_SPEC, + "cpp": CPP_SPEC, "swift": SWIFT_SPEC, } diff --git a/src/jcodemunch_mcp/parser/symbols.py b/src/jcodemunch_mcp/parser/symbols.py index ae81491..837329c 100644 --- a/src/jcodemunch_mcp/parser/symbols.py +++ b/src/jcodemunch_mcp/parser/symbols.py @@ -13,7 +13,7 @@ class Symbol: name: str # Symbol name (e.g., "login") qualified_name: str # Fully qualified (e.g., "MyClass.login") kind: str # "function" | "class" | "method" | "constant" | "type" - language: str # "python" | "javascript" | "typescript" | "go" | "rust" | "java" | "c" + language: str # "python" | "javascript" | "typescript" | "go" | "rust" | "java" | "c" | "cpp" signature: str # Full signature line(s) docstring: str = "" # Extracted docstring (language-specific) summary: str = "" # One-line summary diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 40e1d9a..6ebb6fd 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -212,7 +212,7 @@ async def list_tools() -> list[Tool]: "language": { "type": "string", "description": "Optional filter by language", - "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "swift"] + "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "cpp", "swift"] }, "max_results": { "type": "integer", diff --git a/tests/fixtures/cpp/sample.cpp b/tests/fixtures/cpp/sample.cpp new file mode 100644 index 0000000..6f12074 --- /dev/null +++ b/tests/fixtures/cpp/sample.cpp @@ -0,0 +1,69 @@ +#include +#include + +#define MAX_BUFFER_SIZE 1024 + +/* Manages user data and operations. */ +class UserService { +public: + /* Create a new service instance. */ + UserService(int capacity) : capacity_(capacity) {} + + /* Get a user by their identifier. */ + std::string getUser(int userId) const { + return "user-" + std::to_string(userId); + } + + /* Remove a user from the system. */ + bool deleteUser(int userId) { + return true; + } + +private: + int capacity_; +}; + +/* A 2D coordinate point. */ +struct Point { + double x; + double y; +}; + +/* Status codes for operations. */ +enum Status { + STATUS_OK, + STATUS_ERROR, + STATUS_PENDING +}; + +/* Direction with scoped values. */ +enum class Direction { North, South, East, West }; + +/* A tagged union for results. */ +union Result { + int code; + char *message; +}; + +typedef struct Point PointType; + +/* Authenticate a token string. */ +int authenticate(const char *token) { + return token != nullptr; +} + +/* Add two integers and return the sum. */ +int add(int a, int b) { + return a + b; +} + +// A template function for maximum. +template +T maximum(T a, T b) { + return (a > b) ? a : b; +} + +namespace utils { + // A helper function inside a namespace. + void helper() {} +} diff --git a/tests/test_hardening.py b/tests/test_hardening.py index 2a87270..3e228e5 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -306,6 +306,47 @@ def test_c_function_kind(self): auth = _by_name(symbols, "authenticate") assert auth.kind == "function" + # -- C++ -------------------------------------------------------------- + + def test_cpp_functions(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + grouped = _kinds(symbols) + func_names = {f.name for f in grouped.get("function", [])} + assert "authenticate" in func_names + assert "add" in func_names + + def test_cpp_class(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + cls = _by_name(symbols, "UserService") + assert cls.kind == "class" + + def test_cpp_struct(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + point = _by_name(symbols, "Point") + assert point.kind == "type" + + def test_cpp_enum(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + status = _by_name(symbols, "Status") + assert status.kind == "type" + + def test_cpp_constant(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + const = _by_name(symbols, "MAX_BUFFER_SIZE") + assert const.kind == "constant" + + def test_cpp_method_qualified_name(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + method = _by_name(symbols, "getUser") + assert method.kind == "method" + assert "UserService" in method.qualified_name + # =========================================================================== # 2. Overload Disambiguation @@ -391,6 +432,7 @@ class TestDeterminism: ("dart", "sample.dart"), ("csharp", "sample.cs"), ("c", "sample.c"), + ("cpp", "sample.cpp"), ]) def test_deterministic_ids_and_hashes(self, language, filename): content, fname = _fixture(language, filename) diff --git a/tests/test_languages.py b/tests/test_languages.py index b50763b..fe4e07f 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -306,6 +306,50 @@ class Calculator { ''' +CPP_SOURCE = ''' +#define MAX_BUFFER_SIZE 1024 + +/* Manages user data and operations. */ +class UserService { +public: + /* Create a new service instance. */ + UserService(int capacity) : capacity_(capacity) {} + + /* Get a user by their identifier. */ + std::string getUser(int userId) const { + return "user-" + std::to_string(userId); + } + +private: + int capacity_; +}; + +/* A 2D coordinate point. */ +struct Point { + double x; + double y; +}; + +/* Status codes for operations. */ +enum Status { + STATUS_OK, + STATUS_ERROR, + STATUS_PENDING +}; + +/* Authenticate a token string. */ +int authenticate(const char *token) { + return token != nullptr; +} + +// A template function for maximum. +template +T maximum(T a, T b) { + return (a > b) ? a : b; +} +''' + + C_SOURCE = ''' #define MAX_USERS 100 @@ -601,3 +645,51 @@ def test_parse_c(): assert const.kind == "constant" +def test_parse_cpp(): + """Test C++ parsing.""" + symbols = parse_file(CPP_SOURCE, "sample.cpp", "cpp") + + # Class + cls = next((s for s in symbols if s.name == "UserService" and s.kind == "class"), None) + assert cls is not None + assert "Manages user data" in cls.docstring + + # Methods inside class + get_user = next((s for s in symbols if s.name == "getUser"), None) + assert get_user is not None + assert get_user.kind == "method" + assert get_user.qualified_name == "UserService.getUser" + assert "Get a user by their identifier" in get_user.docstring + + # Constructor (method inside class) + ctor = next((s for s in symbols if s.name == "UserService" and s.kind == "method"), None) + assert ctor is not None + assert ctor.qualified_name == "UserService.UserService" + + # Struct + point = next((s for s in symbols if s.name == "Point"), None) + assert point is not None + assert point.kind == "type" + + # Enum + status = next((s for s in symbols if s.name == "Status"), None) + assert status is not None + assert status.kind == "type" + + # Free function + auth = next((s for s in symbols if s.name == "authenticate"), None) + assert auth is not None + assert auth.kind == "function" + assert "Authenticate a token string" in auth.docstring + + # Template function + maximum = next((s for s in symbols if s.name == "maximum"), None) + assert maximum is not None + assert maximum.kind == "function" + + # Constant + const = next((s for s in symbols if s.name == "MAX_BUFFER_SIZE"), None) + assert const is not None + assert const.kind == "constant" + + From 4d67c3053ee851dd145001b716627b8734a07026 Mon Sep 17 00:00:00 2001 From: jgravelle Date: Thu, 5 Mar 2026 15:54:09 -0600 Subject: [PATCH 05/29] =?UTF-8?q?Bump=20version=20to=200.2.15=20=E2=80=94?= =?UTF-8?q?=20C++=20language=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 058104c..cbb937c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.14" +version = "0.2.15" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" From 56d34b8257674b33f72ef20f808972f7735b5d4a Mon Sep 17 00:00:00 2001 From: Wesley Atwell Date: Thu, 5 Mar 2026 14:35:01 -0700 Subject: [PATCH 06/29] feat: add and harden cpp language support --- LANGUAGE_SUPPORT.md | 6 +- README.md | 6 +- SPEC.md | 4 +- USER_GUIDE.md | 2 +- src/jcodemunch_mcp/parser/extractor.py | 369 +++++++++++++++++++--- src/jcodemunch_mcp/parser/languages.py | 84 ++--- src/jcodemunch_mcp/storage/index_store.py | 22 +- src/jcodemunch_mcp/tools/get_file_tree.py | 15 +- src/jcodemunch_mcp/tools/index_folder.py | 26 +- src/jcodemunch_mcp/tools/index_repo.py | 26 +- tests/fixtures/cpp/sample.cpp | 77 ++--- tests/test_get_file_tree.py | 84 +++++ tests/test_hardening.py | 62 ++++ tests/test_incremental.py | 103 ++++++ tests/test_languages.py | 313 +++++++++++++----- tests/test_server.py | 2 + tests/test_storage.py | 63 ++++ tests/test_tools.py | 4 + 18 files changed, 1028 insertions(+), 240 deletions(-) create mode 100644 tests/test_get_file_tree.py diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index cb03212..8463bb8 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -13,8 +13,10 @@ | PHP | `.php` | tree-sitter-php | function, class, method, type (interface/trait/enum), constant | `#[Attribute]` | `/** */` PHPDoc | PHP 8+ attributes supported; language-file `` XML doc comments | Properties and `const` fields not indexed | -| C | `.c`, `.h` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | -| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | tree-sitter-cpp | function, class, method, type (struct/enum/union), constant | — | `/* */` and `//` comments | Templates unwrapped to inner declaration; namespace contents extracted as top-level | +| C | `.c` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | +| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | tree-sitter-cpp | function, class, method, type (struct/enum/union/alias), constant | — | `/* */` and `//` comments | Namespace symbols are used for qualification but not emitted as standalone symbols | + +\* `.h` uses C++ parsing first, then falls back to C when no C++ symbols are extracted. --- diff --git a/README.md b/README.md index 823ac30..bc5f508 100644 --- a/README.md +++ b/README.md @@ -295,8 +295,10 @@ Every tool response includes a `_meta` envelope with timing, token savings, and | PHP | `.php` | function, class, method, type, constant | | Dart | `.dart` | function, class, method, type | | C# | `.cs` | class, method, type, record | -| C | `.c`, `.h` | function, type, constant | -| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | function, class, method, type, constant | +| C | `.c` | function, type, constant | +| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | function, class, method, type, constant | + +\* `.h` is parsed as C++ first, then falls back to C when no C++ symbols are extracted. See LANGUAGE_SUPPORT.md for full semantics. diff --git a/SPEC.md b/SPEC.md index 2987881..05c32b1 100644 --- a/SPEC.md +++ b/SPEC.md @@ -166,7 +166,7 @@ class Symbol: name: str # Symbol name qualified_name: str # Dot-separated with parent context kind: str # function | class | method | constant | type - language: str # python | javascript | typescript | go | rust | java | php | c + language: str # python | javascript | typescript | go | rust | java | php | dart | csharp | c | cpp signature: str # Full signature line(s) content_hash: str = "" # SHA-256 of source bytes (drift detection) docstring: str = "" @@ -212,7 +212,7 @@ Recursive directory walk with the full security pipeline. ### Filtering Pipeline (Both Paths) -1. **Extension filter** — must be in `LANGUAGE_EXTENSIONS` (.py, .js, .jsx, .ts, .tsx, .go, .rs, .java, .php, .c, .h) +1. **Extension filter** — must be in `LANGUAGE_EXTENSIONS` (.py, .js, .jsx, .ts, .tsx, .go, .rs, .java, .php, .c, .h, .cpp, .cc, .cxx, .hpp, .hh, .hxx) 2. **Skip patterns** — `node_modules/`, `vendor/`, `.git/`, `build/`, `dist/`, lock files, minified files, etc. 3. **`.gitignore`** — respected via the `pathspec` library 4. **Secret detection** — `.env`, `*.pem`, `*.key`, `*.p12`, credentials files excluded diff --git a/USER_GUIDE.md b/USER_GUIDE.md index cbb03aa..4786a45 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -244,7 +244,7 @@ To disable, set `JCODEMUNCH_SHARE_SAVINGS=0` in your MCP server env: Check the URL format (`owner/repo` or full GitHub URL). For private repositories, set `GITHUB_TOKEN`. **"No source files found"** -The repository may not contain supported language files (`.py`, `.js`, `.ts`, `.go`, `.rs`, `.java`, `.c`, `.h`), or files may be excluded by skip patterns. +The repository may not contain supported language files (`.py`, `.js`, `.ts`, `.go`, `.rs`, `.java`, `.c`, `.h`, `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`), or files may be excluded by skip patterns. **Rate limiting** Set `GITHUB_TOKEN` to increase GitHub API limits (5,000 requests/hour vs 60 unauthenticated). diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 0231dfd..5a27ac8 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -1,7 +1,7 @@ """Generic AST symbol extractor using tree-sitter.""" from typing import Optional -from tree_sitter_language_pack import get_language, get_parser +from tree_sitter_language_pack import get_parser from .symbols import Symbol, make_symbol_id, compute_content_hash from .languages import LanguageSpec, LANGUAGE_REGISTRY @@ -21,15 +21,13 @@ def parse_file(content: str, filename: str, language: str) -> list[Symbol]: if language not in LANGUAGE_REGISTRY: return [] - spec = LANGUAGE_REGISTRY[language] source_bytes = content.encode("utf-8") - - # Get parser for this language - parser = get_parser(spec.ts_language) - tree = parser.parse(source_bytes) - - symbols = [] - _walk_tree(tree.root_node, spec, source_bytes, filename, language, symbols, None) + + if language == "cpp": + symbols = _parse_cpp_symbols(source_bytes, filename) + else: + spec = LANGUAGE_REGISTRY[language] + symbols = _parse_with_spec(source_bytes, filename, language, spec) # Disambiguate overloaded symbols (same ID) symbols = _disambiguate_overloads(symbols) @@ -37,6 +35,83 @@ def parse_file(content: str, filename: str, language: str) -> list[Symbol]: return symbols +def _parse_with_spec( + source_bytes: bytes, + filename: str, + language: str, + spec: LanguageSpec, +) -> list[Symbol]: + """Parse source bytes using one language spec.""" + try: + parser = get_parser(spec.ts_language) + tree = parser.parse(source_bytes) + except Exception: + return [] + + symbols: list[Symbol] = [] + _walk_tree(tree.root_node, spec, source_bytes, filename, language, symbols, None) + return symbols + + +def _parse_cpp_symbols(source_bytes: bytes, filename: str) -> list[Symbol]: + """Parse C++ and auto-fallback to C for `.h` files with no C++ symbols.""" + cpp_spec = LANGUAGE_REGISTRY["cpp"] + cpp_symbols: list[Symbol] = [] + cpp_error_nodes = 0 + try: + parser = get_parser(cpp_spec.ts_language) + tree = parser.parse(source_bytes) + cpp_error_nodes = _count_error_nodes(tree.root_node) + _walk_tree(tree.root_node, cpp_spec, source_bytes, filename, "cpp", cpp_symbols, None) + except Exception: + cpp_error_nodes = 10**9 + + # Non-headers are always C++. + if not filename.lower().endswith(".h"): + return cpp_symbols + + # Header auto-detection: parse both C++ and C, prefer better parse quality. + c_spec = LANGUAGE_REGISTRY.get("c") + if not c_spec: + return cpp_symbols + + c_symbols: list[Symbol] = [] + c_error_nodes = 10**9 + try: + c_parser = get_parser(c_spec.ts_language) + c_tree = c_parser.parse(source_bytes) + c_error_nodes = _count_error_nodes(c_tree.root_node) + _walk_tree(c_tree.root_node, c_spec, source_bytes, filename, "c", c_symbols, None) + except Exception: + c_error_nodes = 10**9 + + # If only one parser yields symbols, use that parser's symbols. + if cpp_symbols and not c_symbols: + return cpp_symbols + if c_symbols and not cpp_symbols: + return c_symbols + if not cpp_symbols and not c_symbols: + return cpp_symbols + + # Both yielded symbols: choose fewer parse errors first, then richer symbol output. + if c_error_nodes < cpp_error_nodes: + return c_symbols + if cpp_error_nodes < c_error_nodes: + return cpp_symbols + + # Same error quality: use lexical signal to break ties for `.h`. + if _looks_like_cpp_header(source_bytes): + if len(cpp_symbols) >= len(c_symbols): + return cpp_symbols + else: + return c_symbols + + if len(c_symbols) > len(cpp_symbols): + return c_symbols + + return cpp_symbols + + def _walk_tree( node, spec: LanguageSpec, @@ -44,31 +119,67 @@ def _walk_tree( filename: str, language: str, symbols: list, - parent_symbol: Optional[Symbol] = None + parent_symbol: Optional[Symbol] = None, + scope_parts: Optional[list[str]] = None, + class_scope_depth: int = 0, ): """Recursively walk the AST and extract symbols.""" # Dart: function_signature inside method_signature is handled by method_signature if node.type == "function_signature" and node.parent and node.parent.type == "method_signature": return + is_cpp = language == "cpp" + local_scope_parts = scope_parts or [] + next_parent = parent_symbol + next_class_scope_depth = class_scope_depth + + if is_cpp and node.type == "namespace_definition": + ns_name = _extract_cpp_namespace_name(node, source_bytes) + if ns_name: + local_scope_parts = [*local_scope_parts, ns_name] + # Check if this node is a symbol if node.type in spec.symbol_node_types: - symbol = _extract_symbol( - node, spec, source_bytes, filename, language, parent_symbol - ) - if symbol: - symbols.append(symbol) - parent_symbol = symbol - + # C++ declarations include non-function declarations. Filter those out. + if not (is_cpp and node.type in {"declaration", "field_declaration"} and not _is_cpp_function_declaration(node)): + symbol = _extract_symbol( + node, + spec, + source_bytes, + filename, + language, + parent_symbol, + local_scope_parts, + class_scope_depth, + ) + if symbol: + symbols.append(symbol) + if is_cpp: + if _is_cpp_type_container(node): + next_parent = symbol + next_class_scope_depth = class_scope_depth + 1 + else: + next_parent = symbol + # Check for constant patterns (top-level assignments with UPPER_CASE names) if node.type in spec.constant_patterns and parent_symbol is None: const_symbol = _extract_constant(node, spec, source_bytes, filename, language) if const_symbol: symbols.append(const_symbol) - + # Recurse into children for child in node.children: - _walk_tree(child, spec, source_bytes, filename, language, symbols, parent_symbol) + _walk_tree( + child, + spec, + source_bytes, + filename, + language, + symbols, + next_parent, + local_scope_parts, + next_class_scope_depth, + ) def _extract_symbol( @@ -77,7 +188,9 @@ def _extract_symbol( source_bytes: bytes, filename: str, language: str, - parent_symbol: Optional[Symbol] = None + parent_symbol: Optional[Symbol] = None, + scope_parts: Optional[list[str]] = None, + class_scope_depth: int = 0, ) -> Optional[Symbol]: """Extract a Symbol from an AST node.""" kind = spec.symbol_node_types[node.type] @@ -92,21 +205,38 @@ def _extract_symbol( return None # Build qualified name - if parent_symbol: - qualified_name = f"{parent_symbol.name}.{name}" - kind = "method" if kind == "function" else kind + if language == "cpp": + if parent_symbol: + qualified_name = f"{parent_symbol.qualified_name}.{name}" + elif scope_parts: + qualified_name = ".".join([*scope_parts, name]) + else: + qualified_name = name + if kind == "function" and class_scope_depth > 0: + kind = "method" else: - qualified_name = name - + if parent_symbol: + qualified_name = f"{parent_symbol.name}.{name}" + kind = "method" if kind == "function" else kind + else: + qualified_name = name + + signature_node = node + if language == "cpp": + wrapper = _nearest_cpp_template_wrapper(node) + if wrapper: + signature_node = wrapper + # Build signature - signature = _build_signature(node, spec, source_bytes) - + signature = _build_signature(signature_node, spec, source_bytes) + # Extract docstring - docstring = _extract_docstring(node, spec, source_bytes) - + docstring = _extract_docstring(signature_node, spec, source_bytes) + # Extract decorators decorators = _extract_decorators(node, spec, source_bytes) - + + start_node = signature_node # Dart: function_signature/method_signature have their body as a next sibling end_byte = node.end_byte end_line_num = node.end_point[0] + 1 @@ -117,7 +247,7 @@ def _extract_symbol( end_line_num = next_sib.end_point[0] + 1 # Compute content hash - symbol_bytes = source_bytes[node.start_byte:end_byte] + symbol_bytes = source_bytes[start_node.start_byte:end_byte] c_hash = compute_content_hash(symbol_bytes) # Create symbol @@ -132,10 +262,10 @@ def _extract_symbol( docstring=docstring, decorators=decorators, parent=parent_symbol.id if parent_symbol else None, - line=node.start_point[0] + 1, + line=start_node.start_point[0] + 1, end_line=end_line_num, - byte_offset=node.start_byte, - byte_length=end_byte - node.start_byte, + byte_offset=start_node.start_byte, + byte_length=end_byte - start_node.start_byte, content_hash=c_hash, ) @@ -188,6 +318,9 @@ def _extract_name(node, spec: LanguageSpec, source_bytes: bytes) -> Optional[str name_node = node.child_by_field_name(field_name) if name_node: + if spec.ts_language == "cpp": + return _extract_cpp_name(name_node, source_bytes) + # C function_definition: declarator is a function_declarator, # which wraps the actual identifier. Unwrap recursively. while name_node.type in ("function_declarator", "pointer_declarator", "reference_declarator"): @@ -201,16 +334,86 @@ def _extract_name(node, spec: LanguageSpec, source_bytes: bytes) -> Optional[str return None +def _extract_cpp_name(name_node, source_bytes: bytes) -> Optional[str]: + """Extract C++ symbol names from nested declarators.""" + current = name_node + wrapper_types = { + "function_declarator", + "pointer_declarator", + "reference_declarator", + "array_declarator", + "parenthesized_declarator", + "attributed_declarator", + "init_declarator", + } + + while current.type in wrapper_types: + inner = current.child_by_field_name("declarator") + if not inner: + break + current = inner + + # Prefer typed name children where available. + if current.type in {"qualified_identifier", "scoped_identifier"}: + name_node = current.child_by_field_name("name") + if name_node: + text = source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8").strip() + if text: + return text + + subtree_name = _find_cpp_name_in_subtree(current, source_bytes) + if subtree_name: + return subtree_name + + text = source_bytes[current.start_byte:current.end_byte].decode("utf-8").strip() + return text or None + + +def _find_cpp_name_in_subtree(node, source_bytes: bytes) -> Optional[str]: + """Best-effort extraction of a callable/type name from a declarator subtree.""" + direct_types = {"identifier", "field_identifier", "operator_name", "destructor_name", "type_identifier"} + if node.type in direct_types: + text = source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() + return text or None + + if node.type in {"qualified_identifier", "scoped_identifier"}: + name_node = node.child_by_field_name("name") + if name_node: + return _find_cpp_name_in_subtree(name_node, source_bytes) + + for child in node.children: + if not child.is_named: + continue + found = _find_cpp_name_in_subtree(child, source_bytes) + if found: + return found + return None + + def _build_signature(node, spec: LanguageSpec, source_bytes: bytes) -> str: """Build a clean signature from AST node.""" - # Find the body child to determine where signature ends - body = node.child_by_field_name("body") - - if body: - # Signature is from start of node to start of body - end_byte = body.start_byte + if node.type == "template_declaration": + inner = node.child_by_field_name("declaration") + if not inner: + for child in reversed(node.children): + if child.is_named: + inner = child + break + + if inner: + body = inner.child_by_field_name("body") + end_byte = body.start_byte if body else inner.end_byte + else: + end_byte = node.end_byte else: - end_byte = node.end_byte + # Find the body child to determine where signature ends + body = node.child_by_field_name("body") + + if body: + # Signature is from start of node to start of body + end_byte = body.start_byte + else: + end_byte = node.end_byte sig_bytes = source_bytes[node.start_byte:end_byte] sig_text = sig_bytes.decode("utf-8").strip() @@ -221,6 +424,90 @@ def _build_signature(node, spec: LanguageSpec, source_bytes: bytes) -> str: return sig_text +def _nearest_cpp_template_wrapper(node): + """Return closest enclosing template_declaration (if any).""" + current = node + wrapper = None + while current.parent and current.parent.type == "template_declaration": + wrapper = current.parent + current = current.parent + return wrapper + + +def _is_cpp_type_container(node) -> bool: + """C++ node types that can contain methods.""" + return node.type in {"class_specifier", "struct_specifier", "union_specifier"} + + +def _is_cpp_function_declaration(node) -> bool: + """True if a C++ declaration node is function-like.""" + if node.type not in {"declaration", "field_declaration"}: + return True + + declarator = node.child_by_field_name("declarator") + if not declarator: + return False + return _has_function_declarator(declarator) + + +def _has_function_declarator(node) -> bool: + """Check subtree for function declarator nodes.""" + if node.type in {"function_declarator", "abstract_function_declarator"}: + return True + + for child in node.children: + if child.is_named and _has_function_declarator(child): + return True + return False + + +def _extract_cpp_namespace_name(node, source_bytes: bytes) -> Optional[str]: + """Extract namespace name from a namespace_definition node.""" + name_node = node.child_by_field_name("name") + if not name_node: + for child in node.children: + if child.type in {"namespace_identifier", "identifier"}: + name_node = child + break + + if not name_node: + return None + + name = source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8").strip() + return name or None + + +def _looks_like_cpp_header(source_bytes: bytes) -> bool: + """Heuristic: detect obvious C++ constructs in `.h` content.""" + text = source_bytes.decode("utf-8", errors="ignore") + cpp_markers = ( + "namespace ", + "class ", + "template<", + "template <", + "constexpr", + "noexcept", + "[[", + "std::", + "using ", + "::", + "public:", + "private:", + "protected:", + "operator", + "typename", + ) + return any(marker in text for marker in cpp_markers) + + +def _count_error_nodes(node) -> int: + """Count parser ERROR nodes in a syntax tree subtree.""" + count = 1 if node.type == "ERROR" else 0 + for child in node.children: + count += _count_error_nodes(child) + return count + + def _extract_docstring(node, spec: LanguageSpec, source_bytes: bytes) -> str: """Extract docstring using language-specific strategy.""" if spec.docstring_strategy == "next_sibling_string": diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 4b21ede..313261b 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -61,12 +61,12 @@ class LanguageSpec: ".dart": "dart", ".cs": "csharp", ".c": "c", - ".h": "c", + ".h": "cpp", ".cpp": "cpp", - ".hpp": "cpp", ".cc": "cpp", - ".hh": "cpp", ".cxx": "cpp", + ".hpp": "cpp", + ".hh": "cpp", ".hxx": "cpp", ".swift": "swift", } @@ -394,39 +394,6 @@ class LanguageSpec: ) -# C++ specification -CPP_SPEC = LanguageSpec( - ts_language="cpp", - symbol_node_types={ - "function_definition": "function", - "class_specifier": "class", - "struct_specifier": "type", - "enum_specifier": "type", - "union_specifier": "type", - "type_definition": "type", - }, - name_fields={ - "function_definition": "declarator", - "class_specifier": "name", - "struct_specifier": "name", - "enum_specifier": "name", - "union_specifier": "name", - "type_definition": "declarator", - }, - param_fields={ - "function_definition": "declarator", - }, - return_type_fields={ - "function_definition": "type", - }, - docstring_strategy="preceding_comment", - decorator_node_type=None, - container_node_types=["class_specifier", "struct_specifier"], - constant_patterns=["preproc_def"], - type_patterns=["type_definition", "enum_specifier", "struct_specifier", "union_specifier"], -) - - # Swift specification # Note: tree-sitter-swift uses class_declaration for class/struct/enum/extension; # the declaration_kind child field ("class"/"struct"/"enum"/"extension") disambiguates @@ -457,6 +424,49 @@ class LanguageSpec: ) +# C++ specification +CPP_SPEC = LanguageSpec( + ts_language="cpp", + symbol_node_types={ + "class_specifier": "class", + "struct_specifier": "type", + "union_specifier": "type", + "enum_specifier": "type", + "type_definition": "type", + "alias_declaration": "type", + "function_definition": "function", + "declaration": "function", + "field_declaration": "function", + }, + name_fields={ + "class_specifier": "name", + "struct_specifier": "name", + "union_specifier": "name", + "enum_specifier": "name", + "type_definition": "declarator", + "alias_declaration": "name", + "function_definition": "declarator", + "declaration": "declarator", + "field_declaration": "declarator", + }, + param_fields={ + "function_definition": "declarator", + "declaration": "declarator", + "field_declaration": "declarator", + }, + return_type_fields={ + "function_definition": "type", + "declaration": "type", + "field_declaration": "type", + }, + docstring_strategy="preceding_comment", + decorator_node_type=None, + container_node_types=["class_specifier", "struct_specifier", "union_specifier"], + constant_patterns=["preproc_def"], + type_patterns=["class_specifier", "struct_specifier", "union_specifier", "enum_specifier", "type_definition", "alias_declaration"], +) + + # Language registry LANGUAGE_REGISTRY = { "python": PYTHON_SPEC, @@ -469,6 +479,6 @@ class LanguageSpec: "dart": DART_SPEC, "csharp": CSHARP_SPEC, "c": C_SPEC, - "cpp": CPP_SPEC, "swift": SWIFT_SPEC, + "cpp": CPP_SPEC, } diff --git a/src/jcodemunch_mcp/storage/index_store.py b/src/jcodemunch_mcp/storage/index_store.py index 12abc6c..60a4faa 100644 --- a/src/jcodemunch_mcp/storage/index_store.py +++ b/src/jcodemunch_mcp/storage/index_store.py @@ -369,7 +369,7 @@ def incremental_save( deleted_files: Deleted files (symbols will be removed). new_symbols: Symbols extracted from changed + new files. raw_files: Raw content for changed + new files. - languages: Updated language counts. + languages: Legacy language counts (ignored when symbols are present). git_head: Current HEAD commit hash. Returns: @@ -385,6 +385,9 @@ def incremental_save( # Add new symbols all_symbols_dicts = kept_symbols + [self._symbol_to_dict(s) for s in new_symbols] + recomputed_languages = self._languages_from_symbols(all_symbols_dicts) + if not recomputed_languages and languages: + recomputed_languages = languages # Update source files list old_files = set(index.source_files) @@ -409,7 +412,7 @@ def incremental_save( name=name, indexed_at=datetime.now().isoformat(), source_files=sorted(old_files), - languages=languages, + languages=recomputed_languages, symbols=all_symbols_dicts, index_version=INDEX_VERSION, file_hashes=file_hashes, @@ -446,6 +449,21 @@ def incremental_save( return updated + def _languages_from_symbols(self, symbols: list[dict]) -> dict[str, int]: + """Compute language->file_count from serialized symbols.""" + file_languages: dict[str, str] = {} + for sym in symbols: + file_path = sym.get("file") + language = sym.get("language") + if not file_path or not language: + continue + file_languages.setdefault(file_path, language) + + counts: dict[str, int] = {} + for language in file_languages.values(): + counts[language] = counts.get(language, 0) + 1 + return counts + def list_repos(self) -> list[dict]: """List all indexed repositories.""" repos = [] diff --git a/src/jcodemunch_mcp/tools/get_file_tree.py b/src/jcodemunch_mcp/tools/get_file_tree.py index aa72146..c6ee7b1 100644 --- a/src/jcodemunch_mcp/tools/get_file_tree.py +++ b/src/jcodemunch_mcp/tools/get_file_tree.py @@ -84,6 +84,12 @@ def _build_tree(files: list[str], index, path_prefix: str, include_summaries: bo """Build nested tree from flat file list.""" # Group files by directory root = {} + file_languages: dict[str, str] = {} + for sym in index.symbols: + file_path = sym.get("file") + language = sym.get("language") + if file_path and language and file_path not in file_languages: + file_languages[file_path] = language for file_path in files: # Remove prefix for relative path @@ -101,10 +107,11 @@ def _build_tree(files: list[str], index, path_prefix: str, include_summaries: bo symbol_count = sum(1 for s in index.symbols if s.get("file") == file_path) # Get language - lang = "" - _, ext = os.path.splitext(file_path) - from ..parser import LANGUAGE_EXTENSIONS - lang = LANGUAGE_EXTENSIONS.get(ext, "") + lang = file_languages.get(file_path, "") + if not lang: + _, ext = os.path.splitext(file_path) + from ..parser import LANGUAGE_EXTENSIONS + lang = LANGUAGE_EXTENSIONS.get(ext, "") node = { "path": file_path, diff --git a/src/jcodemunch_mcp/tools/index_folder.py b/src/jcodemunch_mcp/tools/index_folder.py index 46cc83e..27712b1 100644 --- a/src/jcodemunch_mcp/tools/index_folder.py +++ b/src/jcodemunch_mcp/tools/index_folder.py @@ -1,5 +1,6 @@ """Index local folder tool - walk, parse, summarize, save.""" +import hashlib import logging import os from pathlib import Path @@ -301,12 +302,13 @@ def index_folder( # Parse only changed + new files files_to_parse = set(changed) | set(new) new_symbols = [] - languages: dict[str, int] = {} raw_files_subset: dict[str, str] = {} incremental_no_symbols: list[str] = [] for rel_path in files_to_parse: content = current_files[rel_path] + # Track file hashes for changed/new files even when symbol extraction yields none. + raw_files_subset[rel_path] = content ext = os.path.splitext(rel_path)[1] language = LANGUAGE_EXTENSIONS.get(ext) if not language: @@ -315,7 +317,6 @@ def index_folder( symbols = parse_file(content, rel_path, language) if symbols: new_symbols.extend(symbols) - raw_files_subset[rel_path] = content else: incremental_no_symbols.append(rel_path) logger.debug("NO SYMBOLS (incremental): %s", rel_path) @@ -331,13 +332,6 @@ def index_folder( new_symbols = summarize_symbols(new_symbols, use_ai=use_ai_summaries) - # Compute updated language counts from all current files - for rel_path in current_files: - ext = os.path.splitext(rel_path)[1] - lang = LANGUAGE_EXTENSIONS.get(ext) - if lang: - languages[lang] = languages.get(lang, 0) + 1 - from ..storage.index_store import _get_git_head git_head = _get_git_head(folder_path) or "" @@ -345,7 +339,7 @@ def index_folder( owner=owner, name=repo_name, changed_files=changed, new_files=new, deleted_files=deleted, new_symbols=new_symbols, raw_files=raw_files_subset, - languages=languages, git_head=git_head, + languages={}, git_head=git_head, ) result = { @@ -380,7 +374,8 @@ def index_folder( symbols = parse_file(content, rel_path, language) if symbols: all_symbols.extend(symbols) - languages[language] = languages.get(language, 0) + 1 + file_language = symbols[0].language or language + languages[file_language] = languages.get(file_language, 0) + 1 raw_files[rel_path] = content parsed_files.append(rel_path) else: @@ -404,13 +399,20 @@ def index_folder( all_symbols = summarize_symbols(all_symbols, use_ai=use_ai_summaries) # Save index + # Track hashes for all discovered source files so incremental change detection + # does not repeatedly report no-symbol files as "new". + file_hashes = { + fp: hashlib.sha256(content.encode("utf-8")).hexdigest() + for fp, content in current_files.items() + } store.save_index( owner=owner, name=repo_name, source_files=parsed_files, symbols=all_symbols, raw_files=raw_files, - languages=languages + languages=languages, + file_hashes=file_hashes, ) result = { diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index c386abd..7a0db21 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -1,6 +1,7 @@ """Index repository tool - fetch, parse, summarize, save.""" import asyncio +import hashlib import os from typing import Optional from urllib.parse import urlparse @@ -285,11 +286,12 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: files_to_parse = set(changed) | set(new) new_symbols = [] - languages: dict[str, int] = {} raw_files_subset: dict[str, str] = {} for path in files_to_parse: content = current_files[path] + # Track file hashes for changed/new files even when symbol extraction yields none. + raw_files_subset[path] = content _, ext = os.path.splitext(path) language = LANGUAGE_EXTENSIONS.get(ext) if not language: @@ -298,24 +300,16 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: symbols = parse_file(content, path, language) if symbols: new_symbols.extend(symbols) - raw_files_subset[path] = content except Exception: warnings.append(f"Failed to parse {path}") new_symbols = summarize_symbols(new_symbols, use_ai=use_ai_summaries) - # Compute language counts from all current files - for path in current_files: - _, ext = os.path.splitext(path) - lang = LANGUAGE_EXTENSIONS.get(ext) - if lang: - languages[lang] = languages.get(lang, 0) + 1 - updated = store.incremental_save( owner=owner, name=repo, changed_files=changed, new_files=new, deleted_files=deleted, new_symbols=new_symbols, raw_files=raw_files_subset, - languages=languages, + languages={}, ) result = { @@ -345,7 +339,8 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: symbols = parse_file(content, path, language) if symbols: all_symbols.extend(symbols) - languages[language] = languages.get(language, 0) + 1 + file_language = symbols[0].language or language + languages[file_language] = languages.get(file_language, 0) + 1 raw_files[path] = content parsed_files.append(path) except Exception: @@ -359,13 +354,20 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: all_symbols = summarize_symbols(all_symbols, use_ai=use_ai_summaries) # Save index + # Track hashes for all discovered source files so incremental change detection + # does not repeatedly report no-symbol files as "new". + file_hashes = { + fp: hashlib.sha256(content.encode("utf-8")).hexdigest() + for fp, content in current_files.items() + } store.save_index( owner=owner, name=repo, source_files=parsed_files, symbols=all_symbols, raw_files=raw_files, - languages=languages + languages=languages, + file_hashes=file_hashes, ) result = { diff --git a/tests/fixtures/cpp/sample.cpp b/tests/fixtures/cpp/sample.cpp index 6f12074..0cd79cf 100644 --- a/tests/fixtures/cpp/sample.cpp +++ b/tests/fixtures/cpp/sample.cpp @@ -1,69 +1,44 @@ -#include #include +#define MAX_USERS 100 -#define MAX_BUFFER_SIZE 1024 +namespace sample { -/* Manages user data and operations. */ -class UserService { +using UserId = int; + +enum class Status { + STATUS_ACTIVE, + STATUS_DISABLED, +}; + +/* A generic value container. */ +template +class Box { public: - /* Create a new service instance. */ - UserService(int capacity) : capacity_(capacity) {} + explicit Box(T value) : value_(value) {} + ~Box() = default; - /* Get a user by their identifier. */ - std::string getUser(int userId) const { - return "user-" + std::to_string(userId); + T get() const { + return value_; } - /* Remove a user from the system. */ - bool deleteUser(int userId) { - return true; + bool operator==(const Box& other) const { + return value_ == other.value_; } private: - int capacity_; + T value_; }; -/* A 2D coordinate point. */ -struct Point { - double x; - double y; -}; - -/* Status codes for operations. */ -enum Status { - STATUS_OK, - STATUS_ERROR, - STATUS_PENDING -}; - -/* Direction with scoped values. */ -enum class Direction { North, South, East, West }; - -/* A tagged union for results. */ -union Result { - int code; - char *message; -}; - -typedef struct Point PointType; - -/* Authenticate a token string. */ -int authenticate(const char *token) { - return token != nullptr; +/* Identity for generic values. */ +template +T identity(T value) { + return value; } -/* Add two integers and return the sum. */ +/* Add two integers. */ +int add(int a, int b); int add(int a, int b) { return a + b; } -// A template function for maximum. -template -T maximum(T a, T b) { - return (a > b) ? a : b; -} - -namespace utils { - // A helper function inside a namespace. - void helper() {} -} +} // namespace sample diff --git a/tests/test_get_file_tree.py b/tests/test_get_file_tree.py new file mode 100644 index 0000000..e2764ae --- /dev/null +++ b/tests/test_get_file_tree.py @@ -0,0 +1,84 @@ +"""Tests for get_file_tree language labeling behavior.""" + +from jcodemunch_mcp.parser import Symbol +from jcodemunch_mcp.storage import IndexStore +from jcodemunch_mcp.tools.get_file_tree import get_file_tree + + +def _flatten_file_nodes(tree_nodes: list[dict]) -> dict[str, dict]: + """Return {file_path: node} for all file nodes in a tree response.""" + out: dict[str, dict] = {} + for node in tree_nodes: + if node.get("type") == "file": + out[node["path"]] = node + elif node.get("type") == "dir": + out.update(_flatten_file_nodes(node.get("children", []))) + return out + + +def test_get_file_tree_prefers_symbol_language_over_extension(tmp_path): + """A .h file with C symbols should show language='c' instead of extension default.""" + store = IndexStore(base_path=str(tmp_path)) + sym = Symbol( + id="include-api-h::only_c#function", + file="include/api.h", + name="only_c", + qualified_name="only_c", + kind="function", + language="c", + signature="int only_c(void)", + byte_offset=0, + byte_length=20, + ) + + store.save_index( + owner="tree", + name="demo", + source_files=["include/api.h", "src/orphan.cpp"], + symbols=[sym], + raw_files={ + "include/api.h": "int only_c(void) { return 0; }\n", + "src/orphan.cpp": "// no symbols here\n", + }, + languages={"c": 1, "cpp": 1}, + ) + + result = get_file_tree("tree/demo", storage_path=str(tmp_path)) + assert "error" not in result + files = _flatten_file_nodes(result["tree"]) + assert files["include/api.h"]["language"] == "c" + assert files["include/api.h"]["symbol_count"] == 1 + + +def test_get_file_tree_falls_back_to_extension_without_symbol_language(tmp_path): + """When a file has no symbols, get_file_tree should infer language from extension.""" + store = IndexStore(base_path=str(tmp_path)) + sym = Symbol( + id="src-main-cpp::main#function", + file="src/main.cpp", + name="main", + qualified_name="main", + kind="function", + language="cpp", + signature="int main()", + byte_offset=0, + byte_length=10, + ) + + store.save_index( + owner="tree", + name="demo2", + source_files=["src/main.cpp", "include/no_symbols.h"], + symbols=[sym], + raw_files={ + "src/main.cpp": "int main() { return 0; }\n", + "include/no_symbols.h": "/* header with no symbols */\n", + }, + languages={"cpp": 2}, + ) + + result = get_file_tree("tree/demo2", storage_path=str(tmp_path)) + assert "error" not in result + files = _flatten_file_nodes(result["tree"]) + assert files["include/no_symbols.h"]["language"] == "cpp" + assert files["include/no_symbols.h"]["symbol_count"] == 0 diff --git a/tests/test_hardening.py b/tests/test_hardening.py index 3e228e5..89cb258 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -272,6 +272,68 @@ def test_csharp_record(self): symbols = parse_file(content, fname, "csharp") record = _by_name(symbols, "Person") assert record.kind == "class" + + # -- C++ ------------------------------------------------------------- + + def test_cpp_class(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + cls = _by_name(symbols, "Box") + assert cls.kind == "class" + assert "sample" in cls.qualified_name + + def test_cpp_method_qualified_name(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + method = _by_name(symbols, "get") + assert method.kind == "method" + assert "Box" in method.qualified_name + + def test_cpp_alias_and_enum(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + alias = _by_name(symbols, "UserId") + status = _by_name(symbols, "Status") + assert alias.kind == "type" + assert status.kind == "type" + + def test_cpp_constant(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + const = _by_name(symbols, "MAX_USERS") + assert const.kind == "constant" + + def test_cpp_overload_disambiguation(self): + content, fname = _fixture("cpp", "sample.cpp") + symbols = parse_file(content, fname, "cpp") + add_syms = [s for s in symbols if s.name == "add" and s.kind == "function"] + assert len(add_syms) >= 2 + ids = [s.id for s in add_syms] + assert any(i.endswith("~1") for i in ids) + assert any(i.endswith("~2") for i in ids) + + def test_cpp_nested_namespace_qualification(self): + content = """ +namespace a { namespace b { +class Thing { public: int run() const { return 1; } }; +} } +""" + symbols = parse_file(content, "ns.cpp", "cpp") + cls = _by_name(symbols, "Thing") + run = _by_name(symbols, "run") + assert cls.qualified_name == "a.b.Thing" + assert run.qualified_name == "a.b.Thing.run" + + def test_cpp_mixed_header_deterministic(self): + content = """ +class MaybeCpp { public: int Get() const; }; +int only_c(void) { int v[] = (int[]){1,2,3}; return v[0]; } +""" + run1 = parse_file(content, "mixed.h", "cpp") + run2 = parse_file(content, "mixed.h", "cpp") + assert run1 and run2 + assert {s.language for s in run1} == {s.language for s in run2} + # -- C --------------------------------------------------------------- def test_c_functions(self): diff --git a/tests/test_incremental.py b/tests/test_incremental.py index 3ed67a4..15e52dc 100644 --- a/tests/test_incremental.py +++ b/tests/test_incremental.py @@ -4,6 +4,7 @@ from pathlib import Path from jcodemunch_mcp.tools.index_folder import index_folder +from jcodemunch_mcp.storage import IndexStore def _write_py(d: Path, name: str, content: str) -> Path: @@ -13,6 +14,13 @@ def _write_py(d: Path, name: str, content: str) -> Path: return p +def _write_file(d: Path, name: str, content: str) -> Path: + """Write an arbitrary source file into directory d.""" + p = d / name + p.write_text(content, encoding="utf-8") + return p + + class TestIncrementalIndexFolder: """Test incremental indexing through index_folder.""" @@ -122,3 +130,98 @@ def test_incremental_false_does_full_reindex(self, tmp_path): result2 = index_folder(str(src), use_ai_summaries=False, storage_path=str(store)) assert result2["success"] is True assert "incremental" not in result2 + + def test_incremental_reclassifies_h_language_from_c_to_cpp(self, tmp_path): + """Changing .h from C-like to C++-like should update persisted language counts.""" + src = tmp_path / "src" + src.mkdir() + store = tmp_path / "store" + + _write_file(src, "api.h", "int only_c(void) { int v[] = (int[]){1,2,3}; return v[0]; }\n") + + full = index_folder(str(src), use_ai_summaries=False, storage_path=str(store)) + assert full["success"] is True + assert full["languages"] == {"c": 1} + + _write_file( + src, + "api.h", + "namespace demo { class Widget { public: int Get() const; }; }\n", + ) + + inc = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc["success"] is True + assert inc["changed"] == 1 + + idx = IndexStore(base_path=str(store)).load_index("local", src.name) + assert idx is not None + assert idx.languages.get("cpp") == 1 + assert "c" not in idx.languages + + def test_incremental_delete_and_readd_h_updates_language_counts(self, tmp_path): + """Deleting and re-adding .h with different style should keep language counts correct.""" + src = tmp_path / "src" + src.mkdir() + store = tmp_path / "store" + + _write_file(src, "api.h", "int only_c(void) { int v[] = (int[]){1,2,3}; return v[0]; }\n") + _write_file(src, "main.cpp", "namespace demo { int add(int a, int b) { return a + b; } }\n") + + full = index_folder(str(src), use_ai_summaries=False, storage_path=str(store)) + assert full["success"] is True + assert full["languages"] == {"c": 1, "cpp": 1} + + (src / "api.h").unlink() + inc_delete = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc_delete["success"] is True + assert inc_delete["deleted"] == 1 + + idx_after_delete = IndexStore(base_path=str(store)).load_index("local", src.name) + assert idx_after_delete is not None + assert idx_after_delete.languages == {"cpp": 1} + + _write_file(src, "api.h", "namespace demo { class Readded { public: int Go() const; }; }\n") + inc_add = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc_add["success"] is True + assert inc_add["new"] == 1 + + idx_after_add = IndexStore(base_path=str(store)).load_index("local", src.name) + assert idx_after_add is not None + assert idx_after_add.languages == {"cpp": 2} + + def test_incremental_no_symbol_file_not_repeatedly_new(self, tmp_path): + """No-symbol files should not be repeatedly reported as new across incremental runs.""" + src = tmp_path / "src" + src.mkdir() + store = tmp_path / "store" + + _write_file(src, "main.cpp", "int main() { return 0; }\n") + _write_file(src, "no_symbols.h", "/* no symbols here */\n") + + full = index_folder(str(src), use_ai_summaries=False, storage_path=str(store)) + assert full["success"] is True + assert full["no_symbols_count"] >= 1 + + # No changes should be clean on first incremental run. + inc1 = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc1["success"] is True + assert inc1["message"] == "No changes detected" + assert inc1["changed"] == 0 + assert inc1["new"] == 0 + assert inc1["deleted"] == 0 + + # Change the no-symbol file once. + _write_file(src, "no_symbols.h", "/* still no symbols, but changed */\n") + inc2 = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc2["success"] is True + assert inc2["changed"] == 1 + assert inc2["new"] == 0 + assert inc2["deleted"] == 0 + + # Next incremental should be clean again (no repeated churn). + inc3 = index_folder(str(src), use_ai_summaries=False, storage_path=str(store), incremental=True) + assert inc3["success"] is True + assert inc3["message"] == "No changes detected" + assert inc3["changed"] == 0 + assert inc3["new"] == 0 + assert inc3["deleted"] == 0 diff --git a/tests/test_languages.py b/tests/test_languages.py index fe4e07f..48327c9 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -306,28 +306,13 @@ class Calculator { ''' -CPP_SOURCE = ''' -#define MAX_BUFFER_SIZE 1024 - -/* Manages user data and operations. */ -class UserService { -public: - /* Create a new service instance. */ - UserService(int capacity) : capacity_(capacity) {} - - /* Get a user by their identifier. */ - std::string getUser(int userId) const { - return "user-" + std::to_string(userId); - } - -private: - int capacity_; -}; +C_SOURCE = ''' +#define MAX_USERS 100 -/* A 2D coordinate point. */ -struct Point { - double x; - double y; +/* Represents a user in the system. */ +struct User { + char *name; + int age; }; /* Status codes for operations. */ @@ -337,43 +322,128 @@ class UserService { STATUS_PENDING }; +/* Get user by ID. */ +struct User *get_user(int id) { + return NULL; +} + /* Authenticate a token string. */ int authenticate(const char *token) { - return token != nullptr; + return token != NULL; } +''' + + +CPP_SOURCE = ''' +#define MAX_USERS 100 + +namespace sample { + +using UserId = int; -// A template function for maximum. +enum class Status { + STATUS_ACTIVE, + STATUS_DISABLED, +}; + +/* A generic value container. */ +template +class Box { +public: + explicit Box(T value) : value_(value) {} + ~Box() = default; + + T get() const { + return value_; + } + + bool operator==(const Box& other) const { + return value_ == other.value_; + } + +private: + T value_; +}; + +/* Identity for generic values. */ template -T maximum(T a, T b) { - return (a > b) ? a : b; +T identity(T value) { + return value; +} + +/* Add two integers. */ +int add(int a, int b); +int add(int a, int b) { + return a + b; } + +} // namespace sample ''' -C_SOURCE = ''' -#define MAX_USERS 100 +CPP_HEADER_SOURCE = ''' +namespace sample { +class Widget { +public: + Widget(); + ~Widget(); + int Get() const; +}; +} +''' -/* Represents a user in the system. */ -struct User { - char *name; - int age; + +C_ONLY_HEADER_SOURCE = ''' +int only_c(void) { + int values[] = (int[]){1, 2, 3}; + return values[0]; +} +''' + + +CPP_EDGE_SOURCE = ''' +namespace outer { +namespace inner { + +class Ops { +public: + int operator[](int idx) const { return idx; } + int operator()(int x) const { return x; } + explicit operator bool() const { return true; } + int value = 0; }; -/* Status codes for operations. */ -enum Status { - STATUS_OK, - STATUS_ERROR, - STATUS_PENDING +int (*make_callback(int seed))(int) { + return nullptr; +} + +int consume_ref(int& v) { return v; } + +} // namespace inner +} // namespace outer +''' + + +MIXED_HEADER_SOURCE = ''' +class MaybeCpp { +public: + int get() const; }; -/* Get user by ID. */ -struct User *get_user(int id) { - return NULL; +int only_c(void) { + int values[] = (int[]){1, 2, 3}; + return values[0]; } +''' -/* Authenticate a token string. */ -int authenticate(const char *token) { - return token != NULL; + +CXX_KEYWORDS_HEADER_SOURCE = ''' +constexpr int id(int x) noexcept { + return x; +} + +[[nodiscard]] inline int succ(int x) { + return x + 1; } ''' @@ -649,47 +719,142 @@ def test_parse_cpp(): """Test C++ parsing.""" symbols = parse_file(CPP_SOURCE, "sample.cpp", "cpp") - # Class - cls = next((s for s in symbols if s.name == "UserService" and s.kind == "class"), None) + # Namespace-qualified class + cls = next((s for s in symbols if s.name == "Box" and s.kind == "class"), None) assert cls is not None - assert "Manages user data" in cls.docstring - - # Methods inside class - get_user = next((s for s in symbols if s.name == "getUser"), None) - assert get_user is not None - assert get_user.kind == "method" - assert get_user.qualified_name == "UserService.getUser" - assert "Get a user by their identifier" in get_user.docstring + assert cls.qualified_name == "sample.Box" + assert "generic value container" in cls.docstring.lower() - # Constructor (method inside class) - ctor = next((s for s in symbols if s.name == "UserService" and s.kind == "method"), None) + # Constructor + destructor + method + ctor = next((s for s in symbols if s.name == "Box" and s.kind == "method"), None) assert ctor is not None - assert ctor.qualified_name == "UserService.UserService" + assert ctor.qualified_name == "sample.Box.Box" - # Struct - point = next((s for s in symbols if s.name == "Point"), None) - assert point is not None - assert point.kind == "type" + dtor = next((s for s in symbols if s.name == "~Box"), None) + assert dtor is not None + assert dtor.kind == "method" + assert dtor.qualified_name == "sample.Box.~Box" - # Enum - status = next((s for s in symbols if s.name == "Status"), None) - assert status is not None - assert status.kind == "type" + get_method = next((s for s in symbols if s.name == "get"), None) + assert get_method is not None + assert get_method.kind == "method" + assert get_method.qualified_name == "sample.Box.get" - # Free function - auth = next((s for s in symbols if s.name == "authenticate"), None) - assert auth is not None - assert auth.kind == "function" - assert "Authenticate a token string" in auth.docstring + # Operator overload + op = next((s for s in symbols if "operator" in s.name and "==" in s.name), None) + assert op is not None + assert op.kind == "method" - # Template function - maximum = next((s for s in symbols if s.name == "maximum"), None) - assert maximum is not None - assert maximum.kind == "function" + # Type alias + enum + constant + alias = next((s for s in symbols if s.name == "UserId"), None) + assert alias is not None + assert alias.kind == "type" + assert alias.qualified_name == "sample.UserId" - # Constant - const = next((s for s in symbols if s.name == "MAX_BUFFER_SIZE"), None) + enum = next((s for s in symbols if s.name == "Status"), None) + assert enum is not None + assert enum.kind == "type" + assert enum.qualified_name == "sample.Status" + + const = next((s for s in symbols if s.name == "MAX_USERS"), None) assert const is not None assert const.kind == "constant" + # Template function signature should include template prefix. + identity = next((s for s in symbols if s.name == "identity"), None) + assert identity is not None + assert identity.kind == "function" + assert identity.qualified_name == "sample.identity" + assert "template " in identity.signature + + # Overload IDs should be disambiguated. + add_symbols = [s for s in symbols if s.name == "add" and s.kind == "function"] + assert len(add_symbols) >= 2 + add_ids = [s.id for s in add_symbols] + assert any(i.endswith("~1") for i in add_ids) + assert any(i.endswith("~2") for i in add_ids) + + +def test_parse_cpp_header_stays_cpp(): + """C++-style headers should stay in C++ mode.""" + symbols = parse_file(CPP_HEADER_SOURCE, "sample.h", "cpp") + assert symbols + assert all(s.language == "cpp" for s in symbols) + widget = next((s for s in symbols if s.name == "Widget" and s.kind == "class"), None) + assert widget is not None + method = next((s for s in symbols if s.name == "Get"), None) + assert method is not None + assert method.kind == "method" + + +def test_parse_cpp_header_falls_back_to_c(): + """C-only headers should fall back to C when C++ extraction fails.""" + symbols = parse_file(C_ONLY_HEADER_SOURCE, "sample.h", "cpp") + assert symbols + assert all(s.language == "c" for s in symbols) + only_c = next((s for s in symbols if s.name == "only_c"), None) + assert only_c is not None + assert only_c.kind == "function" + + +def test_parse_cpp_edge_operator_and_declarator_names(): + """C++ edge declarator/operator names should be extracted and scoped.""" + symbols = parse_file(CPP_EDGE_SOURCE, "edge.cpp", "cpp") + + cls = next((s for s in symbols if s.name == "Ops" and s.kind == "class"), None) + assert cls is not None + assert cls.qualified_name == "outer.inner.Ops" + + op_index = next((s for s in symbols if "operator[" in s.name), None) + assert op_index is not None + assert op_index.kind == "method" + assert op_index.qualified_name.startswith("outer.inner.Ops.") + + op_call = next((s for s in symbols if "operator(" in s.name), None) + assert op_call is not None + assert op_call.kind == "method" + + op_conv = next((s for s in symbols if "operator bool" in s.name), None) + assert op_conv is not None + assert op_conv.kind == "method" + + callback = next((s for s in symbols if s.name == "make_callback"), None) + assert callback is not None + assert callback.kind == "function" + assert callback.qualified_name == "outer.inner.make_callback" + + consume_ref = next((s for s in symbols if s.name == "consume_ref"), None) + assert consume_ref is not None + assert consume_ref.kind == "function" + assert consume_ref.qualified_name == "outer.inner.consume_ref" + + +def test_parse_cpp_declaration_filter_ignores_variables(): + """Variable declarations should not be indexed as functions in C++.""" + symbols = parse_file(CPP_EDGE_SOURCE, "edge.cpp", "cpp") + variable_names = {"value"} + assert all(s.name not in variable_names for s in symbols) + + +def test_parse_cpp_mixed_header_deterministic_selection(): + """Mixed C/C++ headers should produce deterministic language selection.""" + run1 = parse_file(MIXED_HEADER_SOURCE, "mixed.h", "cpp") + run2 = parse_file(MIXED_HEADER_SOURCE, "mixed.h", "cpp") + + assert run1 and run2 + langs1 = {s.language for s in run1} + langs2 = {s.language for s in run2} + assert langs1 == langs2 + assert len(langs1) == 1 + + +def test_parse_cpp_header_with_cpp_keywords_stays_cpp(): + """C++ keywords in headers should strongly select C++ parsing.""" + symbols = parse_file(CXX_KEYWORDS_HEADER_SOURCE, "keywords.h", "cpp") + assert symbols + assert all(s.language == "cpp" for s in symbols) + names = {s.name for s in symbols if s.kind in {"function", "method"}} + assert "id" in names + assert "succ" in names + diff --git a/tests/test_server.py b/tests/test_server.py index 2c6d72d..7d05a3a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -51,3 +51,5 @@ async def test_search_symbols_tool_schema(): # kind should have enum assert "enum" in props["kind"] assert set(props["kind"]["enum"]) == {"function", "class", "method", "constant", "type"} + assert "enum" in props["language"] + assert "cpp" in props["language"]["enum"] diff --git a/tests/test_storage.py b/tests/test_storage.py index 7e21c80..3a3a5cc 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -260,3 +260,66 @@ def test_codeindex_search(): results = index.search("login", kind="function") assert len(results) > 0 + + +def test_incremental_save_recomputes_languages_from_merged_symbols(tmp_path): + """incremental_save should derive languages from merged symbols, not caller counts.""" + store = IndexStore(base_path=str(tmp_path)) + + py_sym = Symbol( + id="app-py::run#function", + file="app.py", + name="run", + qualified_name="run", + kind="function", + language="python", + signature="def run():", + byte_offset=0, + byte_length=10, + ) + c_sym = Symbol( + id="api-h::only_c#function", + file="api.h", + name="only_c", + qualified_name="only_c", + kind="function", + language="c", + signature="int only_c(void)", + byte_offset=0, + byte_length=20, + ) + + store.save_index( + owner="lang", + name="demo", + source_files=["app.py", "api.h"], + symbols=[py_sym, c_sym], + raw_files={"app.py": "def run():\n pass\n", "api.h": "int only_c(void) { return 0; }\n"}, + languages={"python": 1, "c": 1}, + ) + + cpp_sym = Symbol( + id="api-h::Widget#class", + file="api.h", + name="Widget", + qualified_name="Widget", + kind="class", + language="cpp", + signature="class Widget", + byte_offset=0, + byte_length=12, + ) + + updated = store.incremental_save( + owner="lang", + name="demo", + changed_files=["api.h"], + new_files=[], + deleted_files=[], + new_symbols=[cpp_sym], + raw_files={"api.h": "class Widget { public: int Get() const; };"}, + languages={"c": 99}, # stale caller-provided data; should be ignored + ) + + assert updated is not None + assert updated.languages == {"python": 1, "cpp": 1} diff --git a/tests/test_tools.py b/tests/test_tools.py index e1e07d2..72c082a 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -37,12 +37,16 @@ def test_discover_source_files(): {"path": "node_modules/foo.js", "type": "blob", "size": 500}, {"path": "README.md", "type": "blob", "size": 200}, {"path": "src/utils.py", "type": "blob", "size": 500}, + {"path": "src/engine.cpp", "type": "blob", "size": 700}, + {"path": "include/engine.hpp", "type": "blob", "size": 350}, ] files = discover_source_files(tree_entries, gitignore_content=None) assert "src/main.py" in files assert "src/utils.py" in files + assert "src/engine.cpp" in files + assert "include/engine.hpp" in files assert "node_modules/foo.js" not in files assert "README.md" not in files # Not a source file From 87be09da29ae501b893277a3a51dd69e7d4f9c22 Mon Sep 17 00:00:00 2001 From: Wesley Atwell Date: Thu, 5 Mar 2026 15:04:59 -0700 Subject: [PATCH 07/29] test: align cpp hardening assertions with fixture --- tests/test_hardening.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_hardening.py b/tests/test_hardening.py index 89cb258..e28186a 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -375,20 +375,21 @@ def test_cpp_functions(self): symbols = parse_file(content, fname, "cpp") grouped = _kinds(symbols) func_names = {f.name for f in grouped.get("function", [])} - assert "authenticate" in func_names + assert "identity" in func_names assert "add" in func_names def test_cpp_class(self): content, fname = _fixture("cpp", "sample.cpp") symbols = parse_file(content, fname, "cpp") - cls = _by_name(symbols, "UserService") + cls = _by_name(symbols, "Box") assert cls.kind == "class" + assert "sample" in cls.qualified_name def test_cpp_struct(self): content, fname = _fixture("cpp", "sample.cpp") symbols = parse_file(content, fname, "cpp") - point = _by_name(symbols, "Point") - assert point.kind == "type" + alias = _by_name(symbols, "UserId") + assert alias.kind == "type" def test_cpp_enum(self): content, fname = _fixture("cpp", "sample.cpp") @@ -399,15 +400,15 @@ def test_cpp_enum(self): def test_cpp_constant(self): content, fname = _fixture("cpp", "sample.cpp") symbols = parse_file(content, fname, "cpp") - const = _by_name(symbols, "MAX_BUFFER_SIZE") + const = _by_name(symbols, "MAX_USERS") assert const.kind == "constant" def test_cpp_method_qualified_name(self): content, fname = _fixture("cpp", "sample.cpp") symbols = parse_file(content, fname, "cpp") - method = _by_name(symbols, "getUser") + method = _by_name(symbols, "get") assert method.kind == "method" - assert "UserService" in method.qualified_name + assert "Box" in method.qualified_name # =========================================================================== From dd4df023bd554f755f8ed17b99906c7118163040 Mon Sep 17 00:00:00 2001 From: Neil Greisman Date: Thu, 5 Mar 2026 17:23:35 -0500 Subject: [PATCH 08/29] Add CLI version flag support --- src/jcodemunch_mcp/__init__.py | 2 +- src/jcodemunch_mcp/server.py | 7 +++++++ tests/test_cli.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/jcodemunch_mcp/__init__.py b/src/jcodemunch_mcp/__init__.py index 817143d..17238e8 100644 --- a/src/jcodemunch_mcp/__init__.py +++ b/src/jcodemunch_mcp/__init__.py @@ -1,3 +1,3 @@ """github-codemunch-mcp - Token-efficient MCP server for GitHub source code exploration.""" -__version__ = "0.1.0" +__version__ = "0.2.15" diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 6ebb6fd..ecf2ab0 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -11,6 +11,7 @@ from mcp.server import Server from mcp.types import Tool, TextContent +from . import __version__ from .tools.index_repo import index_repo from .tools.index_folder import index_folder from .tools.list_repos import list_repos @@ -387,6 +388,12 @@ def main(argv: Optional[list[str]] = None): prog="jcodemunch-mcp", description="Run the jCodeMunch MCP stdio server.", ) + parser.add_argument( + "-V", + "--version", + action="version", + version=f"%(prog)s {__version__}", + ) parser.add_argument( "--log-level", default=os.environ.get("JCODEMUNCH_LOG_LEVEL", "WARNING"), diff --git a/tests/test_cli.py b/tests/test_cli.py index bc6f50d..c5a1693 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,3 +14,13 @@ def test_main_help_exits_without_starting_server(capsys): out = capsys.readouterr().out assert "jcodemunch-mcp" in out assert "Run the jCodeMunch MCP stdio server" in out + + +def test_main_version_exits_with_version(capsys): + """`--version` should print package version and exit cleanly.""" + with pytest.raises(SystemExit) as exc: + main(["--version"]) + + assert exc.value.code == 0 + out = capsys.readouterr().out.strip() + assert out.startswith("jcodemunch-mcp ") From 8a31e3d37c6ed5a7b1c92b2909a11892940618f9 Mon Sep 17 00:00:00 2001 From: Allen Guarnes <1196639+allenguarnes@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:12:26 +0800 Subject: [PATCH 09/29] feat: make the indexing file cap configurable Add JCODEMUNCH_MAX_INDEX_FILES so large repos can be indexed with a higher file cap when needed. Apply the limit to both local folder and GitHub repo indexing, and only show truncation notices when files were actually dropped. Cover the env override and truncation behavior in tests. --- README.md | 1 + SECURITY.md | 2 +- src/jcodemunch_mcp/security.py | 32 +++++++++++++++ src/jcodemunch_mcp/tools/index_folder.py | 12 ++++-- src/jcodemunch_mcp/tools/index_repo.py | 25 ++++++++---- tests/test_security.py | 50 ++++++++++++++++++++++- tests/test_tools.py | 52 ++++++++++++++++++++++-- 7 files changed, 158 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 823ac30..0c323b2 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,7 @@ For **LM Studio**, ensure the Local Server is running (usually on port 1234): | `OPENAI_MODEL` | Model name for local LLMs (default: `qwen3-coder`) | No | | `OPENAI_TIMEOUT` | Timeout in seconds for local requests (default: `60.0`) | No | | `CODE_INDEX_PATH` | Custom cache path | No | +| `JCODEMUNCH_MAX_INDEX_FILES`| Maximum files to index per repo/folder (default: `500`) | No | | `JCODEMUNCH_SHARE_SAVINGS` | Set to `0` to disable anonymous community token savings reporting | No | | `JCODEMUNCH_LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` (default: `WARNING`) | No | | `JCODEMUNCH_LOG_FILE` | Path to log file. If unset, logs go to stderr. Use a file to avoid polluting MCP stdio. | No | diff --git a/SECURITY.md b/SECURITY.md index f4e8779..c716070 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -56,7 +56,7 @@ When a secret file is detected, a warning is included in the indexing response. * **Default maximum:** 500 KB per file (configurable via `max_file_size`). * Files exceeding the limit are skipped during discovery. -* A configurable **file count limit** (default: 500 files) prevents runaway indexing of extremely large repositories. +* A configurable **file count limit** (default: 500 files) prevents runaway indexing of extremely large repositories. Can be overridden using the `JCODEMUNCH_MAX_INDEX_FILES` environment variable. --- diff --git a/src/jcodemunch_mcp/security.py b/src/jcodemunch_mcp/security.py index a1f4efa..2f071a4 100644 --- a/src/jcodemunch_mcp/security.py +++ b/src/jcodemunch_mcp/security.py @@ -206,6 +206,38 @@ def safe_decode(data: bytes, encoding: str = "utf-8") -> str: # --- Composite Filters --- DEFAULT_MAX_FILE_SIZE = 500 * 1024 # 500KB +DEFAULT_MAX_INDEX_FILES = 500 +MAX_INDEX_FILES_ENV_VAR = "JCODEMUNCH_MAX_INDEX_FILES" + + +def get_max_index_files(max_files: Optional[int] = None) -> int: + """Resolve the maximum indexed file count from arg or environment. + + Args: + max_files: Explicit override. Must be a positive integer when provided. + + Returns: + Positive file-count limit. Falls back to the default if the environment + variable is unset or invalid. + """ + if max_files is not None: + if max_files <= 0: + raise ValueError("max_files must be a positive integer") + return max_files + + value = os.environ.get(MAX_INDEX_FILES_ENV_VAR) + if value is None: + return DEFAULT_MAX_INDEX_FILES + + try: + parsed = int(value) + except ValueError: + return DEFAULT_MAX_INDEX_FILES + + if parsed <= 0: + return DEFAULT_MAX_INDEX_FILES + + return parsed def should_exclude_file( diff --git a/src/jcodemunch_mcp/tools/index_folder.py b/src/jcodemunch_mcp/tools/index_folder.py index 46cc83e..814f757 100644 --- a/src/jcodemunch_mcp/tools/index_folder.py +++ b/src/jcodemunch_mcp/tools/index_folder.py @@ -17,6 +17,7 @@ is_binary_file, should_exclude_file, DEFAULT_MAX_FILE_SIZE, + get_max_index_files, ) from ..storage import IndexStore from ..summarizer import summarize_symbols @@ -60,7 +61,7 @@ def _load_gitignore(folder_path: Path) -> Optional[pathspec.PathSpec]: def discover_local_files( folder_path: Path, - max_files: int = 500, + max_files: Optional[int] = None, max_size: int = DEFAULT_MAX_FILE_SIZE, extra_ignore_patterns: Optional[list[str]] = None, follow_symlinks: bool = False, @@ -77,6 +78,7 @@ def discover_local_files( Returns: Tuple of (list of Path objects for source files, list of warning strings). """ + max_files = get_max_index_files(max_files) files = [] warnings = [] root = folder_path.resolve() @@ -93,6 +95,7 @@ def discover_local_files( "too_large": 0, "unreadable": 0, "binary": 0, + "file_limit": 0, } # Load .gitignore @@ -194,6 +197,7 @@ def discover_local_files( # File count limit with prioritization if len(files) > max_files: + skip_counts["file_limit"] = len(files) - max_files # Prioritize: src/, lib/, pkg/, cmd/, internal/ first priority_dirs = ["src/", "lib/", "pkg/", "cmd/", "internal/"] @@ -247,11 +251,13 @@ def index_folder( return {"success": False, "error": f"Path is not a directory: {path}"} warnings = [] + max_files = get_max_index_files() try: # Discover source files (with security filtering) source_files, discover_warnings, skip_counts = discover_local_files( folder_path, + max_files=max_files, extra_ignore_patterns=extra_ignore_patterns, follow_symlinks=follow_symlinks, ) @@ -430,8 +436,8 @@ def index_folder( if warnings: result["warnings"] = warnings - if len(source_files) >= 500: - result["note"] = "Folder has many files; indexed first 500" + if skip_counts.get("file_limit", 0) > 0: + result["note"] = f"Folder has many files; indexed first {max_files}" return result diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index c386abd..438ef17 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -8,7 +8,7 @@ import httpx from ..parser import parse_file, LANGUAGE_EXTENSIONS -from ..security import is_secret_file, is_binary_extension +from ..security import is_secret_file, is_binary_extension, get_max_index_files from ..storage import IndexStore from ..summarizer import summarize_symbols @@ -86,9 +86,9 @@ def should_skip_file(path: str) -> bool: def discover_source_files( tree_entries: list[dict], gitignore_content: Optional[str] = None, - max_files: int = 500, + max_files: Optional[int] = None, max_size: int = 500 * 1024 # 500KB -) -> list[str]: +) -> tuple[list[str], bool]: """Discover source files from tree entries. Applies filtering pipeline: @@ -100,6 +100,8 @@ def discover_source_files( 6. File count limit """ import pathspec + + max_files = get_max_index_files(max_files) # Parse gitignore if provided gitignore_spec = None @@ -149,8 +151,10 @@ def discover_source_files( files.append(path) + truncated = len(files) > max_files + # File count limit with prioritization - if len(files) > max_files: + if truncated: # Prioritize: src/, lib/, pkg/, cmd/, internal/ first priority_dirs = ["src/", "lib/", "pkg/", "cmd/", "internal/"] @@ -165,7 +169,7 @@ def priority_key(path): files.sort(key=priority_key) files = files[:max_files] - return files + return files, truncated async def fetch_file_content( @@ -228,6 +232,7 @@ async def index_repo( github_token = os.environ.get("GITHUB_TOKEN") warnings = [] + max_files = get_max_index_files() try: # Fetch tree @@ -244,7 +249,11 @@ async def index_repo( gitignore_content = await fetch_gitignore(owner, repo, github_token) # Discover source files - source_files = discover_source_files(tree_entries, gitignore_content) + source_files, truncated = discover_source_files( + tree_entries, + gitignore_content, + max_files=max_files, + ) if not source_files: return {"success": False, "error": "No source files found"} @@ -381,8 +390,8 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: if warnings: result["warnings"] = warnings - if len(source_files) >= 500: - result["warnings"] = warnings + ["Repository has many files; indexed first 500"] + if truncated: + result["warnings"] = warnings + [f"Repository has many files; indexed first {max_files}"] return result diff --git a/tests/test_security.py b/tests/test_security.py index 6f52ff9..1f8131a 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -4,6 +4,7 @@ import sys import pytest from pathlib import Path +from unittest.mock import patch from jcodemunch_mcp.security import ( validate_path, @@ -16,6 +17,9 @@ should_exclude_file, SECRET_PATTERNS, BINARY_EXTENSIONS, + DEFAULT_MAX_INDEX_FILES, + MAX_INDEX_FILES_ENV_VAR, + get_max_index_files, ) @@ -221,6 +225,24 @@ def test_checks_can_be_disabled(self, tmp_path): assert should_exclude_file(f, tmp_path, check_secrets=False) is None +class TestMaxIndexFilesConfig: + def test_defaults_when_env_is_unset(self): + with patch.dict(os.environ, {}, clear=True): + assert get_max_index_files() == DEFAULT_MAX_INDEX_FILES + + def test_reads_env_override(self): + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "1234"}, clear=True): + assert get_max_index_files() == 1234 + + def test_invalid_env_falls_back_to_default(self): + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "invalid"}, clear=True): + assert get_max_index_files() == DEFAULT_MAX_INDEX_FILES + + def test_non_positive_explicit_value_is_rejected(self): + with pytest.raises(ValueError, match="positive integer"): + get_max_index_files(0) + + # --- Integration: discover_local_files with security --- class TestDiscoverLocalFilesSecure: @@ -277,6 +299,31 @@ def test_extra_ignore_patterns(self, tmp_path): assert "main.py" in names assert "temp.py" not in names + def test_respects_env_file_limit(self, tmp_path): + """Environment override controls local folder file discovery limit.""" + from jcodemunch_mcp.tools.index_folder import discover_local_files + + for i in range(10): + (tmp_path / f"file{i}.py").write_text(f"x = {i}\n") + + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "3"}, clear=False): + files, *_ = discover_local_files(tmp_path) + + assert len(files) == 3 + + def test_exact_env_file_limit_does_not_report_truncation(self, tmp_path): + """Exact file-count matches should not be treated as truncation.""" + from jcodemunch_mcp.tools.index_folder import discover_local_files + + for i in range(3): + (tmp_path / f"file{i}.py").write_text(f"x = {i}\n") + + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "3"}, clear=False): + files, _, skip_counts = discover_local_files(tmp_path) + + assert len(files) == 3 + assert skip_counts["file_limit"] == 0 + @pytest.mark.skipif(sys.platform == "win32", reason="Symlinks unreliable on Windows") def test_symlinks_skipped_by_default(self, tmp_path): """Symlinks are skipped when follow_symlinks=False.""" @@ -308,11 +355,12 @@ def test_secret_files_filtered_in_discovery(self): {"path": "src/utils.py", "type": "blob", "size": 500}, ] - files = discover_source_files(tree_entries) + files, truncated = discover_source_files(tree_entries) assert "src/main.py" in files assert "src/utils.py" in files assert ".env" not in files assert "certs/server.pem" not in files + assert truncated is False # --- Encoding safety in index_store --- diff --git a/tests/test_tools.py b/tests/test_tools.py index e1e07d2..fab234e 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,11 +1,14 @@ """Tests for tools module.""" import pytest +from unittest.mock import patch + from jcodemunch_mcp.tools.index_repo import ( parse_github_url, discover_source_files, should_skip_file, ) +from jcodemunch_mcp.security import MAX_INDEX_FILES_ENV_VAR def test_parse_github_url_full(): @@ -39,12 +42,13 @@ def test_discover_source_files(): {"path": "src/utils.py", "type": "blob", "size": 500}, ] - files = discover_source_files(tree_entries, gitignore_content=None) + files, truncated = discover_source_files(tree_entries, gitignore_content=None) assert "src/main.py" in files assert "src/utils.py" in files assert "node_modules/foo.js" not in files assert "README.md" not in files # Not a source file + assert truncated is False def test_discover_source_files_respects_max(): @@ -54,8 +58,9 @@ def test_discover_source_files_respects_max(): for i in range(1000) ] - files = discover_source_files(tree_entries, max_files=100) + files, truncated = discover_source_files(tree_entries, max_files=100) assert len(files) == 100 + assert truncated is True def test_discover_source_files_prioritizes_src(): @@ -68,8 +73,49 @@ def test_discover_source_files_prioritizes_src(): for i in range(300) ] - files = discover_source_files(tree_entries, max_files=100) + files, truncated = discover_source_files(tree_entries, max_files=100) # Most files should be from src/ src_count = sum(1 for f in files if f.startswith("src/")) assert src_count > 50 # Majority should be src/ + assert truncated is True + + +def test_discover_source_files_uses_env_override(): + """Test that environment override is used when max_files is omitted.""" + tree_entries = [ + {"path": f"file{i}.py", "type": "blob", "size": 100} + for i in range(20) + ] + + with patch.dict("os.environ", {MAX_INDEX_FILES_ENV_VAR: "7"}, clear=False): + files, truncated = discover_source_files(tree_entries) + + assert len(files) == 7 + assert truncated is True + + +def test_discover_source_files_explicit_max_overrides_env(): + """Explicit max_files should win over environment configuration.""" + tree_entries = [ + {"path": f"file{i}.py", "type": "blob", "size": 100} + for i in range(20) + ] + + with patch.dict("os.environ", {MAX_INDEX_FILES_ENV_VAR: "7"}, clear=False): + files, truncated = discover_source_files(tree_entries, max_files=5) + + assert len(files) == 5 + assert truncated is True + + +def test_discover_source_files_exact_limit_is_not_truncated(): + """An exact match to the limit should not be reported as truncation.""" + tree_entries = [ + {"path": f"file{i}.py", "type": "blob", "size": 100} + for i in range(5) + ] + + files, truncated = discover_source_files(tree_entries, max_files=5) + assert len(files) == 5 + assert truncated is False From b6c54901910ea38757b968ea9559d1bf68ddac94 Mon Sep 17 00:00:00 2001 From: Brahm Date: Thu, 5 Mar 2026 16:28:24 -0800 Subject: [PATCH 10/29] feat: add arrow function variable support for JS/TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Index `const foo = () => {}`, `const bar = function() {}`, and `const gen = function*() {}` patterns as function symbols. The name is extracted from the parent variable_declarator node. Changes: - New `_extract_variable_function()` in extractor.py handles variable_declarator nodes with function-like initializers - Remove `arrow_function` from JS/TS symbol_node_types (was silently dropped — caused wasted work on inline callbacks) - Remove dead guard in `_extract_name()` that returned None for arrow_function nodes - 11 new tests covering positive cases (arrow, function expression, generator, exported, typed), negative cases (non-function values, inline callbacks, destructuring), and docstring/signature extraction Co-Authored-By: Claude Opus 4.6 --- src/jcodemunch_mcp/parser/extractor.py | 86 +++++++++++- src/jcodemunch_mcp/parser/languages.py | 2 - tests/test_arrow_functions.py | 178 +++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 7 deletions(-) create mode 100644 tests/test_arrow_functions.py diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 5a27ac8..2031c32 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -161,6 +161,14 @@ def _walk_tree( else: next_parent = symbol + # Check for arrow/function-expression variable assignments in JS/TS + if node.type == "variable_declarator" and language in ("javascript", "typescript"): + var_func = _extract_variable_function( + node, spec, source_bytes, filename, language, parent_symbol + ) + if var_func: + symbols.append(var_func) + # Check for constant patterns (top-level assignments with UPPER_CASE names) if node.type in spec.constant_patterns and parent_symbol is None: const_symbol = _extract_constant(node, spec, source_bytes, filename, language) @@ -274,11 +282,6 @@ def _extract_symbol( def _extract_name(node, spec: LanguageSpec, source_bytes: bytes) -> Optional[str]: """Extract the name from an AST node.""" - # Handle special cases first - if node.type == "arrow_function": - # Arrow functions get name from parent variable_declarator - return None - # Handle type_declaration in Go - name is in type_spec child if node.type == "type_declaration": for child in node.children: @@ -633,6 +636,79 @@ def _extract_decorators(node, spec: LanguageSpec, source_bytes: bytes) -> list[s return decorators +_VARIABLE_FUNCTION_TYPES = frozenset({ + "arrow_function", + "function_expression", + "generator_function", +}) + + +def _extract_variable_function( + node, + spec: LanguageSpec, + source_bytes: bytes, + filename: str, + language: str, + parent_symbol: Optional[Symbol] = None, +) -> Optional[Symbol]: + """Extract a function from `const name = () => {}` or `const name = function() {}`.""" + # node is a variable_declarator + name_node = node.child_by_field_name("name") + if not name_node or name_node.type != "identifier": + return None # destructuring or other non-simple binding + + value_node = node.child_by_field_name("value") + if not value_node or value_node.type not in _VARIABLE_FUNCTION_TYPES: + return None # not a function assignment + + name = source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8") + + kind = "function" + if parent_symbol: + qualified_name = f"{parent_symbol.name}.{name}" + kind = "method" + else: + qualified_name = name + + # Signature: use the full declaration statement (lexical_declaration parent) + # to capture export/const keywords + sig_node = node.parent if node.parent and node.parent.type in ( + "lexical_declaration", "export_statement", "variable_declaration", + ) else node + # Walk up through export_statement wrapper if present + if sig_node.parent and sig_node.parent.type == "export_statement": + sig_node = sig_node.parent + + signature = _build_signature(sig_node, spec, source_bytes) + + # Docstring: look for preceding comment on the declaration statement + doc_node = sig_node + docstring = _extract_docstring(doc_node, spec, source_bytes) + + # Content hash covers the full declaration + start_byte = sig_node.start_byte + end_byte = sig_node.end_byte + symbol_bytes = source_bytes[start_byte:end_byte] + c_hash = compute_content_hash(symbol_bytes) + + return Symbol( + id=make_symbol_id(filename, qualified_name, kind), + file=filename, + name=name, + qualified_name=qualified_name, + kind=kind, + language=language, + signature=signature, + docstring=docstring, + parent=parent_symbol.id if parent_symbol else None, + line=sig_node.start_point[0] + 1, + end_line=sig_node.end_point[0] + 1, + byte_offset=start_byte, + byte_length=end_byte - start_byte, + content_hash=c_hash, + ) + + def _extract_constant( node, spec: LanguageSpec, source_bytes: bytes, filename: str, language: str ) -> Optional[Symbol]: diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 313261b..07a1920 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -104,7 +104,6 @@ class LanguageSpec: "function_declaration": "function", "class_declaration": "class", "method_definition": "method", - "arrow_function": "function", "generator_function_declaration": "function", }, name_fields={ @@ -133,7 +132,6 @@ class LanguageSpec: "function_declaration": "function", "class_declaration": "class", "method_definition": "method", - "arrow_function": "function", "interface_declaration": "type", "type_alias_declaration": "type", "enum_declaration": "type", diff --git a/tests/test_arrow_functions.py b/tests/test_arrow_functions.py new file mode 100644 index 0000000..b74dd31 --- /dev/null +++ b/tests/test_arrow_functions.py @@ -0,0 +1,178 @@ +"""Tests for arrow function variable support in JS/TS.""" + +import pytest +from jcodemunch_mcp.parser import parse_file + + +# --- Arrow function assigned to const --- + +JS_ARROW_BASIC = '''\ +const add = (a, b) => a + b; +''' + +def test_js_arrow_function_indexed(): + """const add = (a, b) => ... should be indexed as a function.""" + symbols = parse_file(JS_ARROW_BASIC, "utils.js", "javascript") + func = next((s for s in symbols if s.name == "add"), None) + assert func is not None + assert func.kind == "function" + assert func.id == "utils.js::add#function" + + +# --- Function expression assigned to const --- + +JS_FUNCTION_EXPRESSION = '''\ +const multiply = function(a, b) { + return a * b; +}; +''' + +def test_js_function_expression_indexed(): + """const multiply = function() {} should be indexed as a function.""" + symbols = parse_file(JS_FUNCTION_EXPRESSION, "math.js", "javascript") + func = next((s for s in symbols if s.name == "multiply"), None) + assert func is not None + assert func.kind == "function" + + +# --- Generator function expression --- + +JS_GENERATOR_EXPRESSION = '''\ +const generate = function*(items) { + for (const item of items) yield item; +}; +''' + +def test_js_generator_expression_indexed(): + """const generate = function*() {} should be indexed as a function.""" + symbols = parse_file(JS_GENERATOR_EXPRESSION, "gen.js", "javascript") + func = next((s for s in symbols if s.name == "generate"), None) + assert func is not None + assert func.kind == "function" + + +# --- Exported arrow function --- + +JS_EXPORTED_ARROW = '''\ +export const validate = (input) => { + return input.length > 0; +}; +''' + +def test_js_exported_arrow_includes_export_in_signature(): + """Exported arrow functions should have 'export' in signature.""" + symbols = parse_file(JS_EXPORTED_ARROW, "validate.js", "javascript") + func = next((s for s in symbols if s.name == "validate"), None) + assert func is not None + assert func.kind == "function" + assert "export" in func.signature + + +# --- TypeScript arrow with type annotation --- + +TS_ARROW_TYPED = '''\ +export const validate = (input: string): boolean => { + return input.length > 0; +}; +''' + +def test_ts_arrow_typed(): + """TypeScript arrow with type annotations should be indexed.""" + symbols = parse_file(TS_ARROW_TYPED, "validate.ts", "typescript") + func = next((s for s in symbols if s.name == "validate"), None) + assert func is not None + assert func.kind == "function" + assert "export" in func.signature + + +# --- Non-function assignments should NOT be indexed --- + +JS_NON_FUNCTION_ASSIGNMENTS = '''\ +const x = 5; +const arr = [1, 2, 3]; +const obj = { key: "value" }; +const str = "hello"; +''' + +def test_non_function_assignments_not_indexed(): + """Plain value assignments should not create function symbols.""" + symbols = parse_file(JS_NON_FUNCTION_ASSIGNMENTS, "vals.js", "javascript") + func_symbols = [s for s in symbols if s.kind == "function"] + assert len(func_symbols) == 0 + + +# --- Inline arrow callbacks should NOT be indexed --- + +JS_INLINE_ARROW = '''\ +[1, 2, 3].map(x => x + 1); +const result = items.filter(item => item.active); +''' + +def test_inline_arrow_not_indexed(): + """Arrow functions not assigned via variable_declarator should not be indexed.""" + symbols = parse_file(JS_INLINE_ARROW, "inline.js", "javascript") + func_symbols = [s for s in symbols if s.kind == "function"] + assert len(func_symbols) == 0 + + +# --- Destructuring should NOT be indexed --- + +JS_DESTRUCTURING = '''\ +const { foo, bar } = require("something"); +const [a, b] = [1, 2]; +''' + +def test_destructuring_not_indexed(): + """Destructuring assignments should not create function symbols.""" + symbols = parse_file(JS_DESTRUCTURING, "destructure.js", "javascript") + func_symbols = [s for s in symbols if s.kind == "function"] + assert len(func_symbols) == 0 + + +# --- Docstring extraction from preceding comment --- + +JS_ARROW_WITH_DOCSTRING = '''\ +/** Adds two numbers together. */ +const add = (a, b) => a + b; +''' + +def test_arrow_docstring_extraction(): + """Arrow function should pick up preceding JSDoc comment.""" + symbols = parse_file(JS_ARROW_WITH_DOCSTRING, "doc.js", "javascript") + func = next((s for s in symbols if s.name == "add"), None) + assert func is not None + assert "Adds two numbers" in func.docstring + + +# --- Signature includes params --- + +JS_ARROW_MULTILINE = '''\ +const processData = (data, options) => { + return data; +}; +''' + +def test_arrow_signature_includes_params(): + """Arrow function signature should include parameter list.""" + symbols = parse_file(JS_ARROW_MULTILINE, "process.js", "javascript") + func = next((s for s in symbols if s.name == "processData"), None) + assert func is not None + assert "data" in func.signature + assert "options" in func.signature + + +# --- Multiple arrow functions in one file --- + +JS_MULTIPLE_ARROWS = '''\ +const foo = () => {}; +function bar() {} +const baz = (x) => x * 2; +''' + +def test_multiple_arrows_with_regular_functions(): + """Both arrow and regular functions should be indexed.""" + symbols = parse_file(JS_MULTIPLE_ARROWS, "multi.js", "javascript") + names = {s.name for s in symbols if s.kind == "function"} + assert "foo" in names + assert "bar" in names + assert "baz" in names From 2157d79e5ce60492ae10de671d0f086c139bae7a Mon Sep 17 00:00:00 2001 From: jgravelle Date: Thu, 5 Mar 2026 18:29:42 -0600 Subject: [PATCH 11/29] =?UTF-8?q?Bump=20version=20to=200.2.16=20=E2=80=94?= =?UTF-8?q?=20C++=20hardening=20+=20configurable=20file=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cbb937c..a29c458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.15" +version = "0.2.16" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" From 322082a3cdedcc92e850f5f544a7ef8437c969c9 Mon Sep 17 00:00:00 2001 From: jgravelle Date: Thu, 5 Mar 2026 18:35:28 -0600 Subject: [PATCH 12/29] =?UTF-8?q?Bump=20version=20to=200.2.17=20=E2=80=94?= =?UTF-8?q?=20arrow=20function=20variable=20support=20for=20JS/TS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a29c458..3566032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.16" +version = "0.2.17" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" From b5139d8396aedc86993d373934fe4575eafcfcea Mon Sep 17 00:00:00 2001 From: Josh Stephens Date: Thu, 5 Mar 2026 17:09:21 -0800 Subject: [PATCH 13/29] feat: add file-level heuristic summaries to index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `file_summaries` field to CodeIndex (INDEX_VERSION 2→3) - Generate heuristic summaries from symbols (e.g., "Defines X class (3 methods)") - Wire summaries into both full and incremental indexing paths - Surface summaries in get_file_tree (include_summaries) and get_file_outline - Use single-pass defaultdict grouping for O(n) symbol-to-file mapping - Backward compatible: v2 indexes load with empty file_summaries - 258 tests passing Co-Authored-By: Claude Opus 4.6 --- src/jcodemunch_mcp/server.py | 2 +- src/jcodemunch_mcp/storage/index_store.py | 71 +++------- src/jcodemunch_mcp/summarizer/__init__.py | 2 + .../summarizer/file_summarize.py | 55 ++++++++ src/jcodemunch_mcp/tools/get_file_outline.py | 8 ++ src/jcodemunch_mcp/tools/index_folder.py | 19 +++ src/jcodemunch_mcp/tools/index_repo.py | 20 ++- tests/test_file_summaries.py | 130 ++++++++++++++++++ tests/test_hardening.py | 2 +- 9 files changed, 254 insertions(+), 55 deletions(-) create mode 100644 src/jcodemunch_mcp/summarizer/file_summarize.py create mode 100644 tests/test_file_summaries.py diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 6ebb6fd..e7a1689 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -114,7 +114,7 @@ async def list_tools() -> list[Tool]: }, "include_summaries": { "type": "boolean", - "description": "Include per-file summaries in the tree output", + "description": "Include file-level summaries in the tree nodes", "default": False } }, diff --git a/src/jcodemunch_mcp/storage/index_store.py b/src/jcodemunch_mcp/storage/index_store.py index 60a4faa..a9b2da8 100644 --- a/src/jcodemunch_mcp/storage/index_store.py +++ b/src/jcodemunch_mcp/storage/index_store.py @@ -13,7 +13,7 @@ from ..parser.symbols import Symbol # Bump this when the index schema changes in an incompatible way. -INDEX_VERSION = 2 +INDEX_VERSION = 3 def _file_hash(content: str) -> str: @@ -49,7 +49,7 @@ class CodeIndex: index_version: int = INDEX_VERSION file_hashes: dict[str, str] = field(default_factory=dict) # file_path -> sha256 git_head: str = "" # HEAD commit hash at index time (for git repos) - file_summaries: dict[str, str] = field(default_factory=dict) # file_path -> summary (populated by #9) + file_summaries: dict[str, str] = field(default_factory=dict) # file_path -> summary def get_symbol(self, symbol_id: str) -> Optional[dict]: """Find a symbol by ID.""" @@ -174,11 +174,7 @@ def _content_dir(self, owner: str, name: str) -> Path: return self.base_path / self._repo_slug(owner, name) def _safe_content_path(self, content_dir: Path, relative_path: str) -> Optional[Path]: - """Resolve a content path and ensure it stays within content_dir. - - Prevents path traversal when writing/reading cached raw files from - untrusted repository paths. - """ + """Resolve a content path and ensure it stays within content_dir.""" try: base = content_dir.resolve() candidate = (content_dir / relative_path).resolve() @@ -198,22 +194,9 @@ def save_index( languages: dict[str, int], file_hashes: Optional[dict[str, str]] = None, git_head: str = "", + file_summaries: Optional[dict[str, str]] = None, ) -> "CodeIndex": - """Save index and raw files to storage. - - Args: - owner: Repository owner. - name: Repository name. - source_files: List of indexed file paths. - symbols: List of Symbol objects. - raw_files: Dict mapping file path to raw content. - languages: Dict mapping language to file count. - file_hashes: Optional precomputed {file_path: sha256} map. - git_head: Optional HEAD commit hash at index time. - - Returns: - CodeIndex object. - """ + """Save index and raw files to storage.""" # Compute file hashes if not provided if file_hashes is None: file_hashes = {fp: _file_hash(content) for fp, content in raw_files.items()} @@ -230,6 +213,7 @@ def save_index( index_version=INDEX_VERSION, file_hashes=file_hashes, git_head=git_head, + file_summaries=file_summaries or {}, ) # Save index JSON atomically: write to temp then rename @@ -237,7 +221,6 @@ def save_index( tmp_path = index_path.with_suffix(".json.tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump(self._index_to_dict(index), f, indent=2) - # Atomic rename (on POSIX; best-effort on Windows) tmp_path.replace(index_path) # Save raw files @@ -280,13 +263,11 @@ def load_index(self, owner: str, name: str) -> Optional[CodeIndex]: index_version=stored_version, file_hashes=data.get("file_hashes", {}), git_head=data.get("git_head", ""), + file_summaries=data.get("file_summaries", {}), ) def get_symbol_content(self, owner: str, name: str, symbol_id: str) -> Optional[str]: - """Read symbol source using stored byte offsets. - - This is O(1) - no re-parsing, just seek + read. - """ + """Read symbol source using stored byte offsets.""" index = self.load_index(owner, name) if not index: return None @@ -314,19 +295,9 @@ def detect_changes( name: str, current_files: dict[str, str], ) -> tuple[list[str], list[str], list[str]]: - """Detect changed, new, and deleted files by comparing hashes. - - Args: - owner: Repository owner. - name: Repository name. - current_files: Dict mapping file_path -> content for current state. - - Returns: - Tuple of (changed_files, new_files, deleted_files). - """ + """Detect changed, new, and deleted files by comparing hashes.""" index = self.load_index(owner, name) if not index: - # No existing index: all files are new return [], list(current_files.keys()), [] old_hashes = index.file_hashes @@ -355,25 +326,12 @@ def incremental_save( raw_files: dict[str, str], languages: dict[str, int], git_head: str = "", + file_summaries: Optional[dict[str, str]] = None, ) -> Optional[CodeIndex]: """Incrementally update an existing index. Removes symbols for deleted/changed files, adds new symbols, updates raw content, and saves atomically. - - Args: - owner: Repository owner. - name: Repository name. - changed_files: Files that changed (symbols will be replaced). - new_files: New files (symbols will be added). - deleted_files: Deleted files (symbols will be removed). - new_symbols: Symbols extracted from changed + new files. - raw_files: Raw content for changed + new files. - languages: Legacy language counts (ignored when symbols are present). - git_head: Current HEAD commit hash. - - Returns: - Updated CodeIndex, or None if no existing index. """ index = self.load_index(owner, name) if not index: @@ -405,6 +363,13 @@ def incremental_save( for fp, content in raw_files.items(): file_hashes[fp] = _file_hash(content) + # Merge file summaries: keep old, remove deleted, update changed/new + merged_summaries = dict(index.file_summaries) + for f in deleted_files: + merged_summaries.pop(f, None) + if file_summaries: + merged_summaries.update(file_summaries) + # Build updated index updated = CodeIndex( repo=f"{owner}/{name}", @@ -417,6 +382,7 @@ def incremental_save( index_version=INDEX_VERSION, file_hashes=file_hashes, git_head=git_head, + file_summaries=merged_summaries, ) # Save atomically @@ -538,4 +504,5 @@ def _index_to_dict(self, index: CodeIndex) -> dict: "index_version": index.index_version, "file_hashes": index.file_hashes, "git_head": index.git_head, + "file_summaries": index.file_summaries, } diff --git a/src/jcodemunch_mcp/summarizer/__init__.py b/src/jcodemunch_mcp/summarizer/__init__.py index 7de65b8..d3648df 100644 --- a/src/jcodemunch_mcp/summarizer/__init__.py +++ b/src/jcodemunch_mcp/summarizer/__init__.py @@ -9,6 +9,7 @@ summarize_symbols_simple, summarize_symbols, ) +from .file_summarize import generate_file_summaries __all__ = [ "BatchSummarizer", @@ -18,4 +19,5 @@ "signature_fallback", "summarize_symbols_simple", "summarize_symbols", + "generate_file_summaries", ] diff --git a/src/jcodemunch_mcp/summarizer/file_summarize.py b/src/jcodemunch_mcp/summarizer/file_summarize.py new file mode 100644 index 0000000..ca87f13 --- /dev/null +++ b/src/jcodemunch_mcp/summarizer/file_summarize.py @@ -0,0 +1,55 @@ +"""Generate per-file heuristic summaries from symbol information.""" + +from ..parser.symbols import Symbol + + +def _heuristic_summary(file_path: str, symbols: list[Symbol]) -> str: + """Generate summary from symbol information.""" + if not symbols: + return "" + + classes = [s for s in symbols if s.kind == "class"] + functions = [s for s in symbols if s.kind == "function"] + methods = [s for s in symbols if s.kind == "method"] + constants = [s for s in symbols if s.kind == "constant"] + types = [s for s in symbols if s.kind == "type"] + + parts = [] + if classes: + for cls in classes[:2]: + method_count = sum(1 for s in symbols if s.parent and s.parent.endswith(f"::{cls.name}#class")) + parts.append(f"Defines {cls.name} class ({method_count} methods)") + if functions: + if len(functions) <= 3: + names = ", ".join(f.name for f in functions) + parts.append(f"Contains {len(functions)} functions: {names}") + else: + names = ", ".join(f.name for f in functions[:3]) + parts.append(f"Contains {len(functions)} functions: {names}, ...") + if types and not parts: + names = ", ".join(t.name for t in types[:3]) + parts.append(f"Defines types: {names}") + if constants and not parts: + parts.append(f"Defines {len(constants)} constants") + + return ". ".join(parts) if parts else "" + + +def generate_file_summaries( + file_symbols: dict[str, list[Symbol]], +) -> dict[str, str]: + """Generate heuristic summaries for each file from symbol data. + + Args: + file_symbols: Maps file path -> list of Symbol objects for that file + + Returns: + Dict mapping file path -> summary string + """ + summaries = {} + + for file_path, symbols in file_symbols.items(): + heuristic = _heuristic_summary(file_path, symbols) + summaries[file_path] = heuristic + + return summaries diff --git a/src/jcodemunch_mcp/tools/get_file_outline.py b/src/jcodemunch_mcp/tools/get_file_outline.py index 4364b2f..bdae9b6 100644 --- a/src/jcodemunch_mcp/tools/get_file_outline.py +++ b/src/jcodemunch_mcp/tools/get_file_outline.py @@ -73,10 +73,18 @@ def get_file_outline( tokens_saved = estimate_savings(raw_bytes, response_bytes) total_saved = record_savings(tokens_saved) + # File-level summary + file_summary = "" + if hasattr(index, "file_summaries") and index.file_summaries: + file_summary = index.file_summaries.get(file_path, "") + elif isinstance(getattr(index, "file_summaries", None), dict): + file_summary = index.file_summaries.get(file_path, "") + return { "repo": f"{owner}/{name}", "file": file_path, "language": language, + "file_summary": file_summary, "symbols": symbols_output, "_meta": { "timing_ms": round(elapsed, 1), diff --git a/src/jcodemunch_mcp/tools/index_folder.py b/src/jcodemunch_mcp/tools/index_folder.py index b90e398..3ec6843 100644 --- a/src/jcodemunch_mcp/tools/index_folder.py +++ b/src/jcodemunch_mcp/tools/index_folder.py @@ -8,9 +8,12 @@ import pathspec +from collections import defaultdict + logger = logging.getLogger(__name__) from ..parser import parse_file, LANGUAGE_EXTENSIONS +from ..summarizer import generate_file_summaries from ..security import ( validate_path, is_symlink_escape, @@ -338,6 +341,12 @@ def index_folder( new_symbols = summarize_symbols(new_symbols, use_ai=use_ai_summaries) + # Generate file summaries for changed/new files + incr_symbols_map = defaultdict(list) + for s in new_symbols: + incr_symbols_map[s.file].append(s) + incr_file_summaries = generate_file_summaries(dict(incr_symbols_map)) + from ..storage.index_store import _get_git_head git_head = _get_git_head(folder_path) or "" @@ -346,6 +355,7 @@ def index_folder( changed_files=changed, new_files=new, deleted_files=deleted, new_symbols=new_symbols, raw_files=raw_files_subset, languages={}, git_head=git_head, + file_summaries=incr_file_summaries, ) result = { @@ -404,6 +414,13 @@ def index_folder( # Generate summaries all_symbols = summarize_symbols(all_symbols, use_ai=use_ai_summaries) + # Generate file-level summaries (single-pass grouping) + file_symbols_map = defaultdict(list) + for s in all_symbols: + file_symbols_map[s.file].append(s) + file_summaries = generate_file_summaries(dict(file_symbols_map)) + + # Save index # Track hashes for all discovered source files so incremental change detection # does not repeatedly report no-symbol files as "new". @@ -419,6 +436,7 @@ def index_folder( raw_files=raw_files, languages=languages, file_hashes=file_hashes, + file_summaries=file_summaries, ) result = { @@ -428,6 +446,7 @@ def index_folder( "indexed_at": store.load_index(owner, repo_name).indexed_at, "file_count": len(parsed_files), "symbol_count": len(all_symbols), + "file_summary_count": sum(1 for v in file_summaries.values() if v), "languages": languages, "files": parsed_files[:20], # Limit files in response "discovery_skip_counts": skip_counts, diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index 21d4cb2..bf965d3 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -8,10 +8,12 @@ import httpx +from collections import defaultdict + from ..parser import parse_file, LANGUAGE_EXTENSIONS from ..security import is_secret_file, is_binary_extension, get_max_index_files from ..storage import IndexStore -from ..summarizer import summarize_symbols +from ..summarizer import summarize_symbols, generate_file_summaries # File patterns to skip @@ -314,11 +316,18 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: new_symbols = summarize_symbols(new_symbols, use_ai=use_ai_summaries) + # Generate file summaries for changed/new files + incr_symbols_map = defaultdict(list) + for s in new_symbols: + incr_symbols_map[s.file].append(s) + incr_file_summaries = generate_file_summaries(dict(incr_symbols_map)) + updated = store.incremental_save( owner=owner, name=repo, changed_files=changed, new_files=new, deleted_files=deleted, new_symbols=new_symbols, raw_files=raw_files_subset, languages={}, + file_summaries=incr_file_summaries, ) result = { @@ -362,6 +371,13 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: # Generate summaries all_symbols = summarize_symbols(all_symbols, use_ai=use_ai_summaries) + # Generate file-level summaries (single-pass grouping) + file_symbols_map = defaultdict(list) + for s in all_symbols: + file_symbols_map[s.file].append(s) + file_summaries = generate_file_summaries(dict(file_symbols_map)) + + # Save index # Track hashes for all discovered source files so incremental change detection # does not repeatedly report no-symbol files as "new". @@ -377,6 +393,7 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: raw_files=raw_files, languages=languages, file_hashes=file_hashes, + file_summaries=file_summaries, ) result = { @@ -385,6 +402,7 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: "indexed_at": store.load_index(owner, repo).indexed_at, "file_count": len(parsed_files), "symbol_count": len(all_symbols), + "file_summary_count": sum(1 for v in file_summaries.values() if v), "languages": languages, "files": parsed_files[:20], # Limit files in response } diff --git a/tests/test_file_summaries.py b/tests/test_file_summaries.py new file mode 100644 index 0000000..00d2223 --- /dev/null +++ b/tests/test_file_summaries.py @@ -0,0 +1,130 @@ +"""Tests for file-level summaries feature.""" + +import json +import tempfile +from pathlib import Path + +import pytest + +from jcodemunch_mcp.parser.symbols import Symbol +from jcodemunch_mcp.summarizer.file_summarize import ( + _heuristic_summary, + generate_file_summaries, +) +from jcodemunch_mcp.storage.index_store import IndexStore, CodeIndex, INDEX_VERSION + + +# --- _heuristic_summary tests --- + +def _make_symbol(name, kind, file="test.py", parent=None): + return Symbol( + id=f"{file}::{name}#{kind}", + file=file, + name=name, + qualified_name=name, + kind=kind, + language="python", + signature=f"def {name}()" if kind == "function" else f"class {name}", + parent=parent, + ) + + +def test_heuristic_summary_single_class(): + cls = _make_symbol("MyClass", "class") + method1 = _make_symbol("method1", "method", parent="test.py::MyClass#class") + method2 = _make_symbol("method2", "method", parent="test.py::MyClass#class") + result = _heuristic_summary("test.py", [cls, method1, method2]) + assert "MyClass" in result + assert "2 methods" in result + + +def test_heuristic_summary_multi_function(): + funcs = [_make_symbol(f"func_{i}", "function") for i in range(5)] + result = _heuristic_summary("test.py", funcs) + assert "5 functions" in result + assert "func_0" in result + assert "..." in result + + +def test_heuristic_summary_few_functions(): + funcs = [_make_symbol(f"func_{i}", "function") for i in range(2)] + result = _heuristic_summary("test.py", funcs) + assert "2 functions" in result + assert "..." not in result + + +def test_heuristic_summary_empty(): + assert _heuristic_summary("test.py", []) == "" + + +# --- generate_file_summaries tests --- + +def test_generate_file_summaries_heuristic(): + """Heuristic summary from symbols.""" + symbols = {"b.py": [_make_symbol("bar", "function", file="b.py")]} + result = generate_file_summaries(symbols) + assert "bar" in result["b.py"] + + +def test_generate_file_summaries_empty_fallback(): + """No symbols -> empty string.""" + symbols = {"c.py": []} + result = generate_file_summaries(symbols) + assert result["c.py"] == "" + + +# --- Storage round-trip tests --- + +def test_storage_roundtrip_file_summaries(): + """Save and load an index with file_summaries.""" + with tempfile.TemporaryDirectory() as tmpdir: + store = IndexStore(base_path=tmpdir) + sym = _make_symbol("hello", "function") + summaries = {"test.py": "Utility functions for testing."} + + index = store.save_index( + owner="test", + name="repo", + source_files=["test.py"], + symbols=[sym], + raw_files={"test.py": "def hello(): pass"}, + languages={"python": 1}, + file_summaries=summaries, + ) + + assert index.file_summaries == summaries + + loaded = store.load_index("test", "repo") + assert loaded is not None + assert loaded.file_summaries == summaries + + +def test_backward_compat_v2_index(): + """Loading a v2 index (without file_summaries) should succeed with empty dict.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Write a v2-style index file directly + index_data = { + "repo": "test/repo", + "owner": "test", + "name": "repo", + "indexed_at": "2024-01-01T00:00:00", + "source_files": ["main.py"], + "languages": {"python": 1}, + "symbols": [], + "index_version": 2, + "file_hashes": {}, + "git_head": "", + } + index_path = Path(tmpdir) / "test-repo.json" + with open(index_path, "w") as f: + json.dump(index_data, f) + + store = IndexStore(base_path=tmpdir) + loaded = store.load_index("test", "repo") + assert loaded is not None + assert loaded.file_summaries == {} + + +def test_index_version_bumped(): + """INDEX_VERSION should be 3.""" + assert INDEX_VERSION == 3 diff --git a/tests/test_hardening.py b/tests/test_hardening.py index e28186a..b66e5ee 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -647,7 +647,7 @@ def test_saved_index_has_current_version(self, tmp_path): ) assert index.index_version == INDEX_VERSION - assert index.index_version == 2 + assert index.index_version == 3 def test_load_preserves_version(self, tmp_path): store = IndexStore(base_path=str(tmp_path)) From 6df79d01fb3544038e4fc682e34e9df8f37ee485 Mon Sep 17 00:00:00 2001 From: Jeffrey Vandenborne Date: Fri, 6 Mar 2026 06:40:21 +0100 Subject: [PATCH 14/29] feat: add Elixir language support (.ex, .exs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements full Elixir symbol extraction via a custom AST walker, bypassing the standard LanguageSpec approach because Elixir's tree-sitter grammar is homoiconic (all constructs are generic `call`/`unary_operator` nodes with no dedicated function/class types). Symbols extracted: - defmodule / defimpl → class - defprotocol → type - def / defp / defmacro / defmacrop / defguard / defguardp → method (inside module) or function (top-level) - @type / @typep / @opaque / @callback → type - @doc / @moduledoc string content captured as docstrings - Nested modules handled via recursive descent with parent tracking - Multi-clause functions disambiguated by existing _disambiguate_overloads() Key implementation note: child_by_field_name("arguments") returns None in the Elixir grammar even though the node exists as a named child. Added _get_elixir_args() helper that finds it by type iteration. Files changed: - src/jcodemunch_mcp/parser/languages.py: ELIXIR_SPEC + extensions - src/jcodemunch_mcp/parser/extractor.py: _parse_elixir_symbols() + walker - src/jcodemunch_mcp/server.py: add "elixir" to language enum - tests/fixtures/elixir/sample.ex: comprehensive fixture - tests/test_languages.py: test_parse_elixir() with 13 assertions - tests/test_hardening.py: determinism + per-language extraction tests - LANGUAGE_SUPPORT.md / README.md: updated language tables Co-Authored-By: Claude Sonnet 4.6 --- LANGUAGE_SUPPORT.md | 1 + README.md | 1 + src/jcodemunch_mcp/parser/extractor.py | 413 +++++++++++++++++++++++++ src/jcodemunch_mcp/parser/languages.py | 22 ++ src/jcodemunch_mcp/server.py | 2 +- tests/fixtures/elixir/sample.ex | 56 ++++ tests/test_hardening.py | 41 +++ tests/test_languages.py | 107 +++++++ 8 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/elixir/sample.ex diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index 8463bb8..db533d4 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -15,6 +15,7 @@ | C# | `.cs` | tree-sitter-csharp | class (class/record), method (method/constructor), type (interface/enum/struct/delegate) | `[Attribute]` | `/// ` XML doc comments | Properties and `const` fields not indexed | | C | `.c` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | tree-sitter-cpp | function, class, method, type (struct/enum/union/alias), constant | — | `/* */` and `//` comments | Namespace symbols are used for qualification but not emitted as standalone symbols | +| Elixir | `.ex`, `.exs` | tree-sitter-elixir | class (defmodule/defimpl), type (defprotocol/@type/@callback), method (def/defp/defmacro/defguard inside module), function (top-level def) | — | `@doc`/`@moduledoc` strings | Homoiconic grammar; custom walker required. `defstruct`, `use`, `import`, `alias` not indexed | \* `.h` uses C++ parsing first, then falls back to C when no C++ symbols are extracted. diff --git a/README.md b/README.md index 224500b..a01c763 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ Every tool response includes a `_meta` envelope with timing, token savings, and | C# | `.cs` | class, method, type, record | | C | `.c` | function, type, constant | | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | function, class, method, type, constant | +| Elixir | `.ex`, `.exs` | class (module/impl), type (protocol/@type/@callback), method, function | \* `.h` is parsed as C++ first, then falls back to C when no C++ symbols are extracted. diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 2031c32..149df2e 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -25,6 +25,8 @@ def parse_file(content: str, filename: str, language: str) -> list[Symbol]: if language == "cpp": symbols = _parse_cpp_symbols(source_bytes, filename) + elif language == "elixir": + symbols = _parse_elixir_symbols(source_bytes, filename) else: spec = LANGUAGE_REGISTRY[language] symbols = _parse_with_spec(source_bytes, filename, language, spec) @@ -814,6 +816,417 @@ def _extract_constant( return None +# =========================================================================== +# Elixir custom extractor +# =========================================================================== + +def _get_elixir_args(node) -> Optional[object]: + """Return the `arguments` named child of an Elixir AST node. + + The Elixir tree-sitter grammar does not expose `arguments` as a named + field (only `target` is a named field on `call` nodes), so we find it by + scanning named_children. + """ + for child in node.named_children: + if child.type == "arguments": + return child + return None + + +def _parse_elixir_symbols(source_bytes: bytes, filename: str) -> list[Symbol]: + """Parse Elixir source and return extracted symbols.""" + from .languages import LANGUAGE_REGISTRY + spec = LANGUAGE_REGISTRY["elixir"] + try: + parser = get_parser(spec.ts_language) + tree = parser.parse(source_bytes) + except Exception: + return [] + + symbols: list[Symbol] = [] + _walk_elixir(tree.root_node, source_bytes, filename, symbols, None) + return symbols + + +def _walk_elixir(node, source_bytes: bytes, filename: str, symbols: list, parent_symbol: Optional[Symbol]): + """Recursively walk Elixir AST and extract symbols.""" + if node.type == "call": + target = node.child_by_field_name("target") + if target is None: + _walk_elixir_children(node, source_bytes, filename, symbols, parent_symbol) + return + + keyword = source_bytes[target.start_byte:target.end_byte].decode("utf-8").strip() + + if keyword in ("defmodule", "defprotocol", "defimpl"): + sym = _extract_elixir_module(node, keyword, source_bytes, filename, parent_symbol) + if sym: + symbols.append(sym) + # Recurse into do_block with this module as parent + do_block = _find_elixir_do_block(node) + if do_block: + _walk_elixir_children(do_block, source_bytes, filename, symbols, sym) + return + + if keyword in ("def", "defp", "defmacro", "defmacrop", "defguard", "defguardp"): + sym = _extract_elixir_function(node, keyword, source_bytes, filename, parent_symbol) + if sym: + symbols.append(sym) + return + + elif node.type == "unary_operator": + operator_node = None + for child in node.children: + if not child.is_named and source_bytes[child.start_byte:child.end_byte].decode("utf-8") == "@": + operator_node = child + break + + if operator_node is not None: + # Find the inner call (e.g. `type name :: expr` or `callback name :: expr`) + inner_call = None + for child in node.children: + if child.is_named: + inner_call = child + break + + if inner_call and inner_call.type == "call": + inner_target = inner_call.child_by_field_name("target") + if inner_target: + attr_name = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() + if attr_name in ("type", "typep", "opaque"): + sym = _extract_elixir_type_attribute(node, attr_name, inner_call, source_bytes, filename, parent_symbol) + if sym: + symbols.append(sym) + return + if attr_name == "callback": + sym = _extract_elixir_callback(node, inner_call, source_bytes, filename, parent_symbol) + if sym: + symbols.append(sym) + return + + _walk_elixir_children(node, source_bytes, filename, symbols, parent_symbol) + + +def _walk_elixir_children(node, source_bytes: bytes, filename: str, symbols: list, parent_symbol: Optional[Symbol]): + for child in node.children: + _walk_elixir(child, source_bytes, filename, symbols, parent_symbol) + + +def _find_elixir_do_block(call_node) -> Optional[object]: + """Find the do_block child of a call node (from arguments or direct child).""" + arguments = call_node.child_by_field_name("arguments") + if arguments: + for child in arguments.children: + if child.type == "do_block": + return child + # Sometimes do_block is a direct child + for child in call_node.children: + if child.type == "do_block": + return child + return None + + +def _extract_elixir_module(node, keyword: str, source_bytes: bytes, filename: str, parent_symbol: Optional[Symbol]) -> Optional[Symbol]: + """Extract a defmodule/defprotocol/defimpl symbol.""" + arguments = _get_elixir_args(node) + if arguments is None: + return None + + # For defimpl, find `alias` (implemented module) + `for:` target + if keyword == "defimpl": + name = _extract_elixir_defimpl_name(arguments, source_bytes, parent_symbol) + else: + name = _extract_elixir_alias_name(arguments, source_bytes) + + if not name: + return None + + kind = "type" if keyword == "defprotocol" else "class" + + if parent_symbol: + qualified_name = f"{parent_symbol.qualified_name}.{name}" + else: + qualified_name = name + + # Signature: everything up to the do_block + signature = _build_elixir_signature(node, source_bytes) + + # Moduledoc: look inside do_block + do_block = _find_elixir_do_block(node) + docstring = _extract_elixir_moduledoc(do_block, source_bytes) if do_block else "" + + symbol_bytes = source_bytes[node.start_byte:node.end_byte] + c_hash = compute_content_hash(symbol_bytes) + + return Symbol( + id=make_symbol_id(filename, qualified_name, kind), + file=filename, + name=name, + qualified_name=qualified_name, + kind=kind, + language="elixir", + signature=signature, + docstring=docstring, + parent=parent_symbol.id if parent_symbol else None, + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=c_hash, + ) + + +def _extract_elixir_alias_name(arguments, source_bytes: bytes) -> Optional[str]: + """Extract module name from an `alias` node in arguments.""" + for child in arguments.children: + if child.type == "alias": + return source_bytes[child.start_byte:child.end_byte].decode("utf-8").strip() + # Sometimes the module name is an `atom` (rare) or `identifier` + if child.type in ("identifier", "atom"): + return source_bytes[child.start_byte:child.end_byte].decode("utf-8").strip() + return None + + +def _extract_elixir_defimpl_name(arguments, source_bytes: bytes, parent_symbol: Optional[Symbol]) -> Optional[str]: + """Build a name for defimpl: '.' or just the protocol name.""" + # First child is usually the protocol alias + proto_name = None + for_name = None + + for child in arguments.children: + if child.type == "alias" and proto_name is None: + proto_name = source_bytes[child.start_byte:child.end_byte].decode("utf-8").strip() + # `for:` keyword argument: keywords > pair > (atom "for") + alias + if child.type == "keywords": + for pair in child.children: + if pair.type == "pair": + key_node = pair.child_by_field_name("key") + val_node = pair.child_by_field_name("value") + if key_node and val_node: + key_text = source_bytes[key_node.start_byte:key_node.end_byte].decode("utf-8").strip() + if key_text in ("for", "for:"): + for_name = source_bytes[val_node.start_byte:val_node.end_byte].decode("utf-8").strip() + + if proto_name and for_name: + # e.g. Printable.Integer + return f"{proto_name}.{for_name}" + return proto_name + + +def _extract_elixir_function(node, keyword: str, source_bytes: bytes, filename: str, parent_symbol: Optional[Symbol]) -> Optional[Symbol]: + """Extract a def/defp/defmacro/defmacrop/defguard/defguardp symbol.""" + arguments = _get_elixir_args(node) + if arguments is None: + return None + + # First named child in arguments is a `call` node (the function head) + func_call = None + for child in arguments.children: + if child.is_named: + func_call = child + break + + if func_call is None: + return None + + # Handle guard: `def foo(x) when is_integer(x)` — binary_operator `when` + actual_call = func_call + if func_call.type == "binary_operator": + left = func_call.child_by_field_name("left") + if left: + actual_call = left + + name = _extract_elixir_call_name(actual_call, source_bytes) + if not name: + return None + + # Determine kind based on parent context + if parent_symbol and parent_symbol.kind in ("class", "type"): + kind = "method" + else: + kind = "function" + + if parent_symbol: + qualified_name = f"{parent_symbol.qualified_name}.{name}" + else: + qualified_name = name + + signature = _build_elixir_signature(node, source_bytes) + docstring = _extract_elixir_doc(node, source_bytes) + + symbol_bytes = source_bytes[node.start_byte:node.end_byte] + c_hash = compute_content_hash(symbol_bytes) + + return Symbol( + id=make_symbol_id(filename, qualified_name, kind), + file=filename, + name=name, + qualified_name=qualified_name, + kind=kind, + language="elixir", + signature=signature, + docstring=docstring, + parent=parent_symbol.id if parent_symbol else None, + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=c_hash, + ) + + +def _extract_elixir_call_name(call_node, source_bytes: bytes) -> Optional[str]: + """Extract the function name from a call node's target.""" + if call_node.type == "call": + target = call_node.child_by_field_name("target") + if target: + return source_bytes[target.start_byte:target.end_byte].decode("utf-8").strip() + if call_node.type == "identifier": + return source_bytes[call_node.start_byte:call_node.end_byte].decode("utf-8").strip() + return None + + +def _build_elixir_signature(node, source_bytes: bytes) -> str: + """Build function/module signature: text up to the do_block.""" + do_block = _find_elixir_do_block(node) + if do_block: + sig_bytes = source_bytes[node.start_byte:do_block.start_byte] + else: + sig_bytes = source_bytes[node.start_byte:node.end_byte] + return sig_bytes.decode("utf-8").strip().rstrip(",").strip() + + +def _extract_elixir_doc(node, source_bytes: bytes) -> str: + """Walk backward through prev_named_sibling looking for @doc attribute.""" + prev = node.prev_named_sibling + while prev is not None: + if prev.type == "unary_operator": + # Check if this is @doc, @spec, @impl, etc. + inner = None + for child in prev.children: + if child.is_named: + inner = child + break + if inner and inner.type == "call": + inner_target = inner.child_by_field_name("target") + if inner_target: + attr = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() + if attr == "doc": + return _extract_elixir_string_arg(inner, source_bytes) + if attr in ("spec", "impl"): + # Skip @spec and @impl, keep walking back + prev = prev.prev_named_sibling + continue + # Some other attribute — stop + break + elif prev.type in ("comment",): + prev = prev.prev_named_sibling + continue + else: + break + return "" + + +def _extract_elixir_moduledoc(do_block, source_bytes: bytes) -> str: + """Find @moduledoc inside a do_block and extract its string content.""" + if do_block is None: + return "" + for child in do_block.children: + if child.type == "unary_operator": + inner = None + for c in child.children: + if c.is_named: + inner = c + break + if inner and inner.type == "call": + inner_target = inner.child_by_field_name("target") + if inner_target: + attr = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() + if attr == "moduledoc": + return _extract_elixir_string_arg(inner, source_bytes) + return "" + + +def _extract_elixir_string_arg(call_node, source_bytes: bytes) -> str: + """Extract string content from @doc/@moduledoc argument (handles both "" and \"\"\"\"\"\").""" + arguments = _get_elixir_args(call_node) + if arguments is None: + return "" + + for child in arguments.children: + if child.type == "string": + text = source_bytes[child.start_byte:child.end_byte].decode("utf-8") + return _strip_quotes(text) + # @doc false → boolean node, not a string + return "" + + +def _extract_elixir_type_attribute(node, attr_name: str, inner_call, source_bytes: bytes, filename: str, parent_symbol: Optional[Symbol]) -> Optional[Symbol]: + """Extract @type/@typep/@opaque as type symbols.""" + # inner_call is the `call` inside `@type name :: expr` + arguments = _get_elixir_args(inner_call) + if arguments is None: + return None + + # The first named child is a `binary_operator` with `::` operator + # whose left side is the type name (possibly a call for parameterized types) + for child in arguments.children: + if child.is_named: + name = _extract_elixir_type_name(child, source_bytes) + if not name: + return None + + kind = "type" + if parent_symbol: + qualified_name = f"{parent_symbol.qualified_name}.{name}" + else: + qualified_name = name + + sig = source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() + symbol_bytes = source_bytes[node.start_byte:node.end_byte] + c_hash = compute_content_hash(symbol_bytes) + + return Symbol( + id=make_symbol_id(filename, qualified_name, kind), + file=filename, + name=name, + qualified_name=qualified_name, + kind=kind, + language="elixir", + signature=sig, + parent=parent_symbol.id if parent_symbol else None, + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=c_hash, + ) + return None + + +def _extract_elixir_callback(node, inner_call, source_bytes: bytes, filename: str, parent_symbol: Optional[Symbol]) -> Optional[Symbol]: + """Extract @callback as a type symbol.""" + return _extract_elixir_type_attribute(node, "callback", inner_call, source_bytes, filename, parent_symbol) + + +def _extract_elixir_type_name(type_expr_node, source_bytes: bytes) -> Optional[str]: + """Extract just the name from a type expression like `name :: type` or `name(params) :: type`.""" + # `binary_operator` with `::` — left side is the name + if type_expr_node.type == "binary_operator": + left = type_expr_node.child_by_field_name("left") + if left: + return _extract_elixir_type_name(left, source_bytes) + # Plain `call` like `name(params)` — name is the target + if type_expr_node.type == "call": + target = type_expr_node.child_by_field_name("target") + if target: + return source_bytes[target.start_byte:target.end_byte].decode("utf-8").strip() + # Plain identifier + if type_expr_node.type in ("identifier", "atom"): + return source_bytes[type_expr_node.start_byte:type_expr_node.end_byte].decode("utf-8").strip() + return None + + def _disambiguate_overloads(symbols: list[Symbol]) -> list[Symbol]: """Append ordinal suffix to symbols with duplicate IDs. diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 07a1920..80f5cd7 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -69,6 +69,8 @@ class LanguageSpec: ".hh": "cpp", ".hxx": "cpp", ".swift": "swift", + ".ex": "elixir", + ".exs": "elixir", } @@ -465,6 +467,25 @@ class LanguageSpec: ) +# Elixir specification +# NOTE: Elixir's tree-sitter grammar is homoiconic — all constructs (defmodule, +# def, defp, defmacro, @doc, @type, etc.) are represented as generic `call` or +# `unary_operator` nodes. Custom extraction is performed in extractor.py via +# _parse_elixir_symbols(); the fields below are intentionally empty. +ELIXIR_SPEC = LanguageSpec( + ts_language="elixir", + symbol_node_types={}, + name_fields={}, + param_fields={}, + return_type_fields={}, + docstring_strategy="elixir", + decorator_node_type=None, + container_node_types=[], + constant_patterns=[], + type_patterns=[], +) + + # Language registry LANGUAGE_REGISTRY = { "python": PYTHON_SPEC, @@ -479,4 +500,5 @@ class LanguageSpec: "c": C_SPEC, "swift": SWIFT_SPEC, "cpp": CPP_SPEC, + "elixir": ELIXIR_SPEC, } diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 6ebb6fd..1644e2a 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -212,7 +212,7 @@ async def list_tools() -> list[Tool]: "language": { "type": "string", "description": "Optional filter by language", - "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "cpp", "swift"] + "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "cpp", "swift", "elixir"] }, "max_results": { "type": "integer", diff --git a/tests/fixtures/elixir/sample.ex b/tests/fixtures/elixir/sample.ex new file mode 100644 index 0000000..2744a3e --- /dev/null +++ b/tests/fixtures/elixir/sample.ex @@ -0,0 +1,56 @@ +defmodule MyApp.Calculator do + @moduledoc """ + A simple calculator module. + """ + + @type result :: {:ok, number()} | {:error, String.t()} + + @doc """ + Adds two numbers together. + """ + @spec add(number(), number()) :: number() + def add(a, b) do + a + b + end + + @doc """ + Subtracts b from a. + """ + def subtract(a, b), do: a - b + + @doc false + defp validate(x) when is_number(x) do + {:ok, x} + end + + defmacro debug(expr) do + quote do + IO.inspect(unquote(expr)) + end + end +end + +defmodule MyApp.Types do + @moduledoc "Type definitions." + + @type name :: String.t() + @typep age :: non_neg_integer() + @opaque token :: binary() + + defguard is_positive(x) when is_number(x) and x > 0 +end + +defprotocol MyApp.Printable do + @moduledoc "Protocol for printable types." + + @callback render(term()) :: String.t() + + @doc "Renders the value as a string." + def to_string(value) +end + +defimpl MyApp.Printable, for: Integer do + def to_string(value) do + Integer.to_string(value) + end +end diff --git a/tests/test_hardening.py b/tests/test_hardening.py index e28186a..5eed7b4 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -410,6 +410,46 @@ def test_cpp_method_qualified_name(self): assert method.kind == "method" assert "Box" in method.qualified_name + # -- Elixir ---------------------------------------------------------- + + def test_elixir_module(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + mod = _by_name(symbols, "MyApp.Calculator") + assert mod.kind == "class" + assert mod.language == "elixir" + + def test_elixir_method(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + method = _by_name(symbols, "add") + assert method.kind == "method" + assert method.parent is not None + + def test_elixir_private_function(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + func = _by_name(symbols, "validate") + assert func.kind == "method" + + def test_elixir_protocol(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + proto = _by_name(symbols, "MyApp.Printable") + assert proto.kind == "type" + + def test_elixir_type_attribute(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + t = _by_name(symbols, "result") + assert t.kind == "type" + + def test_elixir_qualified_names(self): + content, fname = _fixture("elixir", "sample.ex") + symbols = parse_file(content, fname, "elixir") + add = _by_name(symbols, "add") + assert add.qualified_name == "MyApp.Calculator.add" + # =========================================================================== # 2. Overload Disambiguation @@ -496,6 +536,7 @@ class TestDeterminism: ("csharp", "sample.cs"), ("c", "sample.c"), ("cpp", "sample.cpp"), + ("elixir", "sample.ex"), ]) def test_deterministic_ids_and_hashes(self, language, filename): content, fname = _fixture(language, filename) diff --git a/tests/test_languages.py b/tests/test_languages.py index 48327c9..5e5aef7 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -775,6 +775,113 @@ def test_parse_cpp(): assert any(i.endswith("~2") for i in add_ids) +ELIXIR_SOURCE = ''' +defmodule MyApp.Calculator do + @moduledoc """ + A simple calculator module. + """ + + @type result :: {:ok, number()} | {:error, String.t()} + + @doc """ + Adds two numbers together. + """ + def add(a, b) do + a + b + end + + @doc false + defp validate(x) when is_number(x) do + {:ok, x} + end + + defmacro debug(expr) do + quote do: IO.inspect(unquote(expr)) + end +end + +defmodule MyApp.Types do + @type name :: String.t() + defguard is_positive(x) when is_number(x) and x > 0 +end + +defprotocol MyApp.Printable do + @callback render(term()) :: String.t() + def to_string(value) +end + +defimpl MyApp.Printable, for: Integer do + def to_string(value), do: Integer.to_string(value) +end +''' + + +def test_parse_elixir(): + """Test Elixir parsing.""" + symbols = parse_file(ELIXIR_SOURCE, "sample.ex", "elixir") + + # Module + calc = next((s for s in symbols if s.name == "MyApp.Calculator"), None) + assert calc is not None + assert calc.kind == "class" + assert "simple calculator" in calc.docstring.lower() + + # Method inside module (def) + add = next((s for s in symbols if s.name == "add"), None) + assert add is not None + assert add.kind == "method" + assert add.qualified_name == "MyApp.Calculator.add" + assert add.parent == calc.id + assert "Adds two numbers" in add.docstring + + # Private method (defp) + validate = next((s for s in symbols if s.name == "validate"), None) + assert validate is not None + assert validate.kind == "method" + assert validate.qualified_name == "MyApp.Calculator.validate" + + # Macro (defmacro) + macro = next((s for s in symbols if s.name == "debug"), None) + assert macro is not None + assert macro.kind == "method" + + # Type alias (@type) + result_type = next((s for s in symbols if s.name == "result"), None) + assert result_type is not None + assert result_type.kind == "type" + assert result_type.qualified_name == "MyApp.Calculator.result" + + # Guard (defguard in separate module) + guard = next((s for s in symbols if s.name == "is_positive"), None) + assert guard is not None + assert guard.kind == "method" + assert guard.qualified_name == "MyApp.Types.is_positive" + + # @type in Types module + name_type = next((s for s in symbols if s.name == "name"), None) + assert name_type is not None + assert name_type.kind == "type" + + # Protocol (defprotocol) + protocol = next((s for s in symbols if s.name == "MyApp.Printable"), None) + assert protocol is not None + assert protocol.kind == "type" + + # @callback inside protocol + callback = next((s for s in symbols if s.name == "render"), None) + assert callback is not None + assert callback.kind == "type" + + # Protocol implementation (defimpl) + impl = next((s for s in symbols if "Printable" in s.qualified_name and s.kind == "class"), None) + assert impl is not None + + # Function inside impl + to_str = next((s for s in symbols if s.name == "to_string"), None) + assert to_str is not None + assert to_str.kind == "method" + + def test_parse_cpp_header_stays_cpp(): """C++-style headers should stay in C++ mode.""" symbols = parse_file(CPP_HEADER_SOURCE, "sample.h", "cpp") From ee99818cb08c8546a2328d0591988a97c0def702 Mon Sep 17 00:00:00 2001 From: Jeffrey Vandenborne Date: Fri, 6 Mar 2026 10:25:21 +0100 Subject: [PATCH 15/29] refactor: simplify Elixir parser with shared helpers and constants - Add _node_text(), _first_named_child(), _get_elixir_attr_name(), and _make_elixir_symbol() helpers to eliminate repeated patterns - Add _ELIXIR_*_KW frozenset constants to replace inline tuples - Simplify _find_elixir_do_block() by removing dead arguments branch - Remove redundant @ operator check in _walk_elixir (attr_name check suffices) - Inline trivial _extract_elixir_callback() delegation - Fix single-element tuple comparison and duplicate byte slice - Remove redundant LANGUAGE_REGISTRY import inside function body - Net: -46 lines, no behavioral changes (all 257 tests pass) Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 7 + src/jcodemunch_mcp/parser/extractor.py | 217 ++++++++++--------------- 2 files changed, 89 insertions(+), 135 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3566032..41a203d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,3 +33,10 @@ exclude = [".claude/"] [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "pytest-cov>=7.0.0", +] diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 149df2e..acbc8dc 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -833,9 +833,59 @@ def _get_elixir_args(node) -> Optional[object]: return None +# --- Elixir keyword sets --- +_ELIXIR_MODULE_KW = frozenset({"defmodule", "defprotocol", "defimpl"}) +_ELIXIR_FUNCTION_KW = frozenset({"def", "defp", "defmacro", "defmacrop", "defguard", "defguardp"}) +_ELIXIR_TYPE_ATTRS = frozenset({"type", "typep", "opaque"}) +_ELIXIR_SKIP_ATTRS = frozenset({"spec", "impl"}) + + +def _node_text(node, source_bytes: bytes) -> str: + """Return the decoded text of a tree-sitter node.""" + return source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() + + +def _first_named_child(node): + """Return the first named child of a node, or None.""" + return next((c for c in node.children if c.is_named), None) + + +def _get_elixir_attr_name(node, source_bytes: bytes) -> Optional[str]: + """Extract the attribute name from a unary_operator `@attr` node, or None.""" + inner = _first_named_child(node) + if inner and inner.type == "call": + target = inner.child_by_field_name("target") + if target: + return _node_text(target, source_bytes) + return None + + +def _make_elixir_symbol( + node, source_bytes: bytes, filename: str, name: str, qualified_name: str, + kind: str, parent_symbol: Optional[Symbol], signature: str, docstring: str = "" +) -> Symbol: + """Construct a Symbol for an Elixir node.""" + symbol_bytes = source_bytes[node.start_byte:node.end_byte] + return Symbol( + id=make_symbol_id(filename, qualified_name, kind), + file=filename, + name=name, + qualified_name=qualified_name, + kind=kind, + language="elixir", + signature=signature, + docstring=docstring, + parent=parent_symbol.id if parent_symbol else None, + line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + byte_offset=node.start_byte, + byte_length=node.end_byte - node.start_byte, + content_hash=compute_content_hash(symbol_bytes), + ) + + def _parse_elixir_symbols(source_bytes: bytes, filename: str) -> list[Symbol]: """Parse Elixir source and return extracted symbols.""" - from .languages import LANGUAGE_REGISTRY spec = LANGUAGE_REGISTRY["elixir"] try: parser = get_parser(spec.ts_language) @@ -856,9 +906,9 @@ def _walk_elixir(node, source_bytes: bytes, filename: str, symbols: list, parent _walk_elixir_children(node, source_bytes, filename, symbols, parent_symbol) return - keyword = source_bytes[target.start_byte:target.end_byte].decode("utf-8").strip() + keyword = _node_text(target, source_bytes) - if keyword in ("defmodule", "defprotocol", "defimpl"): + if keyword in _ELIXIR_MODULE_KW: sym = _extract_elixir_module(node, keyword, source_bytes, filename, parent_symbol) if sym: symbols.append(sym) @@ -868,41 +918,23 @@ def _walk_elixir(node, source_bytes: bytes, filename: str, symbols: list, parent _walk_elixir_children(do_block, source_bytes, filename, symbols, sym) return - if keyword in ("def", "defp", "defmacro", "defmacrop", "defguard", "defguardp"): + if keyword in _ELIXIR_FUNCTION_KW: sym = _extract_elixir_function(node, keyword, source_bytes, filename, parent_symbol) if sym: symbols.append(sym) return elif node.type == "unary_operator": - operator_node = None - for child in node.children: - if not child.is_named and source_bytes[child.start_byte:child.end_byte].decode("utf-8") == "@": - operator_node = child - break - - if operator_node is not None: - # Find the inner call (e.g. `type name :: expr` or `callback name :: expr`) - inner_call = None - for child in node.children: - if child.is_named: - inner_call = child - break - - if inner_call and inner_call.type == "call": - inner_target = inner_call.child_by_field_name("target") - if inner_target: - attr_name = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() - if attr_name in ("type", "typep", "opaque"): - sym = _extract_elixir_type_attribute(node, attr_name, inner_call, source_bytes, filename, parent_symbol) - if sym: - symbols.append(sym) - return - if attr_name == "callback": - sym = _extract_elixir_callback(node, inner_call, source_bytes, filename, parent_symbol) - if sym: - symbols.append(sym) - return + inner_call = _first_named_child(node) + if inner_call and inner_call.type == "call": + inner_target = inner_call.child_by_field_name("target") + if inner_target: + attr_name = _node_text(inner_target, source_bytes) + if attr_name in _ELIXIR_TYPE_ATTRS or attr_name == "callback": + sym = _extract_elixir_type_attribute(node, attr_name, inner_call, source_bytes, filename, parent_symbol) + if sym: + symbols.append(sym) + return _walk_elixir_children(node, source_bytes, filename, symbols, parent_symbol) @@ -913,13 +945,7 @@ def _walk_elixir_children(node, source_bytes: bytes, filename: str, symbols: lis def _find_elixir_do_block(call_node) -> Optional[object]: - """Find the do_block child of a call node (from arguments or direct child).""" - arguments = call_node.child_by_field_name("arguments") - if arguments: - for child in arguments.children: - if child.type == "do_block": - return child - # Sometimes do_block is a direct child + """Find the do_block child of a call node.""" for child in call_node.children: if child.type == "do_block": return child @@ -955,25 +981,7 @@ def _extract_elixir_module(node, keyword: str, source_bytes: bytes, filename: st do_block = _find_elixir_do_block(node) docstring = _extract_elixir_moduledoc(do_block, source_bytes) if do_block else "" - symbol_bytes = source_bytes[node.start_byte:node.end_byte] - c_hash = compute_content_hash(symbol_bytes) - - return Symbol( - id=make_symbol_id(filename, qualified_name, kind), - file=filename, - name=name, - qualified_name=qualified_name, - kind=kind, - language="elixir", - signature=signature, - docstring=docstring, - parent=parent_symbol.id if parent_symbol else None, - line=node.start_point[0] + 1, - end_line=node.end_point[0] + 1, - byte_offset=node.start_byte, - byte_length=node.end_byte - node.start_byte, - content_hash=c_hash, - ) + return _make_elixir_symbol(node, source_bytes, filename, name, qualified_name, kind, parent_symbol, signature, docstring) def _extract_elixir_alias_name(arguments, source_bytes: bytes) -> Optional[str]: @@ -1020,12 +1028,7 @@ def _extract_elixir_function(node, keyword: str, source_bytes: bytes, filename: return None # First named child in arguments is a `call` node (the function head) - func_call = None - for child in arguments.children: - if child.is_named: - func_call = child - break - + func_call = _first_named_child(arguments) if func_call is None: return None @@ -1054,25 +1057,7 @@ def _extract_elixir_function(node, keyword: str, source_bytes: bytes, filename: signature = _build_elixir_signature(node, source_bytes) docstring = _extract_elixir_doc(node, source_bytes) - symbol_bytes = source_bytes[node.start_byte:node.end_byte] - c_hash = compute_content_hash(symbol_bytes) - - return Symbol( - id=make_symbol_id(filename, qualified_name, kind), - file=filename, - name=name, - qualified_name=qualified_name, - kind=kind, - language="elixir", - signature=signature, - docstring=docstring, - parent=parent_symbol.id if parent_symbol else None, - line=node.start_point[0] + 1, - end_line=node.end_point[0] + 1, - byte_offset=node.start_byte, - byte_length=node.end_byte - node.start_byte, - content_hash=c_hash, - ) + return _make_elixir_symbol(node, source_bytes, filename, name, qualified_name, kind, parent_symbol, signature, docstring) def _extract_elixir_call_name(call_node, source_bytes: bytes) -> Optional[str]: @@ -1101,25 +1086,17 @@ def _extract_elixir_doc(node, source_bytes: bytes) -> str: prev = node.prev_named_sibling while prev is not None: if prev.type == "unary_operator": - # Check if this is @doc, @spec, @impl, etc. - inner = None - for child in prev.children: - if child.is_named: - inner = child - break - if inner and inner.type == "call": - inner_target = inner.child_by_field_name("target") - if inner_target: - attr = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() - if attr == "doc": - return _extract_elixir_string_arg(inner, source_bytes) - if attr in ("spec", "impl"): - # Skip @spec and @impl, keep walking back - prev = prev.prev_named_sibling - continue + attr = _get_elixir_attr_name(prev, source_bytes) + if attr == "doc": + inner = _first_named_child(prev) + return _extract_elixir_string_arg(inner, source_bytes) + if attr in _ELIXIR_SKIP_ATTRS: + # Skip @spec and @impl, keep walking back + prev = prev.prev_named_sibling + continue # Some other attribute — stop break - elif prev.type in ("comment",): + elif prev.type == "comment": prev = prev.prev_named_sibling continue else: @@ -1133,17 +1110,9 @@ def _extract_elixir_moduledoc(do_block, source_bytes: bytes) -> str: return "" for child in do_block.children: if child.type == "unary_operator": - inner = None - for c in child.children: - if c.is_named: - inner = c - break - if inner and inner.type == "call": - inner_target = inner.child_by_field_name("target") - if inner_target: - attr = source_bytes[inner_target.start_byte:inner_target.end_byte].decode("utf-8").strip() - if attr == "moduledoc": - return _extract_elixir_string_arg(inner, source_bytes) + if _get_elixir_attr_name(child, source_bytes) == "moduledoc": + inner = _first_named_child(child) + return _extract_elixir_string_arg(inner, source_bytes) return "" @@ -1182,33 +1151,11 @@ def _extract_elixir_type_attribute(node, attr_name: str, inner_call, source_byte else: qualified_name = name - sig = source_bytes[node.start_byte:node.end_byte].decode("utf-8").strip() - symbol_bytes = source_bytes[node.start_byte:node.end_byte] - c_hash = compute_content_hash(symbol_bytes) - - return Symbol( - id=make_symbol_id(filename, qualified_name, kind), - file=filename, - name=name, - qualified_name=qualified_name, - kind=kind, - language="elixir", - signature=sig, - parent=parent_symbol.id if parent_symbol else None, - line=node.start_point[0] + 1, - end_line=node.end_point[0] + 1, - byte_offset=node.start_byte, - byte_length=node.end_byte - node.start_byte, - content_hash=c_hash, - ) + sig = _node_text(node, source_bytes) + return _make_elixir_symbol(node, source_bytes, filename, name, qualified_name, kind, parent_symbol, sig) return None -def _extract_elixir_callback(node, inner_call, source_bytes: bytes, filename: str, parent_symbol: Optional[Symbol]) -> Optional[Symbol]: - """Extract @callback as a type symbol.""" - return _extract_elixir_type_attribute(node, "callback", inner_call, source_bytes, filename, parent_symbol) - - def _extract_elixir_type_name(type_expr_node, source_bytes: bytes) -> Optional[str]: """Extract just the name from a type expression like `name :: type` or `name(params) :: type`.""" # `binary_operator` with `::` — left side is the name From 919ee23b4f6b4dafac8774ba628956d09048c207 Mon Sep 17 00:00:00 2001 From: Neil Greisman Date: Fri, 6 Mar 2026 05:10:43 -0500 Subject: [PATCH 16/29] Derive __version__ from package metadata --- src/jcodemunch_mcp/__init__.py | 7 ++++++- src/jcodemunch_mcp/server.py | 7 +++++++ tests/test_cli.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/jcodemunch_mcp/__init__.py b/src/jcodemunch_mcp/__init__.py index 817143d..34876f8 100644 --- a/src/jcodemunch_mcp/__init__.py +++ b/src/jcodemunch_mcp/__init__.py @@ -1,3 +1,8 @@ """github-codemunch-mcp - Token-efficient MCP server for GitHub source code exploration.""" -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("jcodemunch-mcp") +except PackageNotFoundError: + __version__ = "unknown" diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 6ebb6fd..ecf2ab0 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -11,6 +11,7 @@ from mcp.server import Server from mcp.types import Tool, TextContent +from . import __version__ from .tools.index_repo import index_repo from .tools.index_folder import index_folder from .tools.list_repos import list_repos @@ -387,6 +388,12 @@ def main(argv: Optional[list[str]] = None): prog="jcodemunch-mcp", description="Run the jCodeMunch MCP stdio server.", ) + parser.add_argument( + "-V", + "--version", + action="version", + version=f"%(prog)s {__version__}", + ) parser.add_argument( "--log-level", default=os.environ.get("JCODEMUNCH_LOG_LEVEL", "WARNING"), diff --git a/tests/test_cli.py b/tests/test_cli.py index bc6f50d..c5a1693 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,3 +14,13 @@ def test_main_help_exits_without_starting_server(capsys): out = capsys.readouterr().out assert "jcodemunch-mcp" in out assert "Run the jCodeMunch MCP stdio server" in out + + +def test_main_version_exits_with_version(capsys): + """`--version` should print package version and exit cleanly.""" + with pytest.raises(SystemExit) as exc: + main(["--version"]) + + assert exc.value.code == 0 + out = capsys.readouterr().out.strip() + assert out.startswith("jcodemunch-mcp ") From 0612c34498404cc86673103d076668c8785e3fd3 Mon Sep 17 00:00:00 2001 From: jgravelle Date: Fri, 6 Mar 2026 05:58:52 -0600 Subject: [PATCH 17/29] =?UTF-8?q?Bump=20version=20to=200.2.18=20=E2=80=94?= =?UTF-8?q?=20file-level=20summaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- src/jcodemunch_mcp/storage/index_store.py | 6 +++++- src/jcodemunch_mcp/tools/get_file_outline.py | 7 +------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3566032..c0fcee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.17" +version = "0.2.18" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" diff --git a/src/jcodemunch_mcp/storage/index_store.py b/src/jcodemunch_mcp/storage/index_store.py index a9b2da8..e1bdc3b 100644 --- a/src/jcodemunch_mcp/storage/index_store.py +++ b/src/jcodemunch_mcp/storage/index_store.py @@ -174,7 +174,11 @@ def _content_dir(self, owner: str, name: str) -> Path: return self.base_path / self._repo_slug(owner, name) def _safe_content_path(self, content_dir: Path, relative_path: str) -> Optional[Path]: - """Resolve a content path and ensure it stays within content_dir.""" + """Resolve a content path and ensure it stays within content_dir. + + Prevents path traversal when writing/reading cached raw files from + untrusted repository paths. + """ try: base = content_dir.resolve() candidate = (content_dir / relative_path).resolve() diff --git a/src/jcodemunch_mcp/tools/get_file_outline.py b/src/jcodemunch_mcp/tools/get_file_outline.py index bdae9b6..006959a 100644 --- a/src/jcodemunch_mcp/tools/get_file_outline.py +++ b/src/jcodemunch_mcp/tools/get_file_outline.py @@ -73,12 +73,7 @@ def get_file_outline( tokens_saved = estimate_savings(raw_bytes, response_bytes) total_saved = record_savings(tokens_saved) - # File-level summary - file_summary = "" - if hasattr(index, "file_summaries") and index.file_summaries: - file_summary = index.file_summaries.get(file_path, "") - elif isinstance(getattr(index, "file_summaries", None), dict): - file_summary = index.file_summaries.get(file_path, "") + file_summary = index.file_summaries.get(file_path, "") return { "repo": f"{owner}/{name}", From 9264038dc4db3080c8d6200b664a845af87a26fd Mon Sep 17 00:00:00 2001 From: jgravelle Date: Fri, 6 Mar 2026 06:04:33 -0600 Subject: [PATCH 18/29] =?UTF-8?q?Bump=20version=20to=200.2.19=20=E2=80=94?= =?UTF-8?q?=20Elixir=20support=20+=20--version=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f26d2f..e541a42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.18" +version = "0.2.19" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" From 2f929cffa3118247882ab25d45dc3d77dee954bd Mon Sep 17 00:00:00 2001 From: jgravelle Date: Fri, 6 Mar 2026 06:11:39 -0600 Subject: [PATCH 19/29] feat: add Ruby language support (.rb, .rake) Extracts class, module (type), instance methods, singleton methods (def self.foo), and top-level functions via standard LanguageSpec. Preceding # comments captured as docstrings. Closes #31 Co-Authored-By: Claude Sonnet 4.6 --- LANGUAGE_SUPPORT.md | 1 + README.md | 1 + pyproject.toml | 2 +- src/jcodemunch_mcp/parser/languages.py | 31 +++++++++ src/jcodemunch_mcp/server.py | 2 +- tests/fixtures/ruby/sample.rb | 46 +++++++++++++ tests/test_hardening.py | 36 +++++++++++ tests/test_languages.py | 89 ++++++++++++++++++++++++++ 8 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/ruby/sample.rb diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index db533d4..e0e263e 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -16,6 +16,7 @@ | C | `.c` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | tree-sitter-cpp | function, class, method, type (struct/enum/union/alias), constant | — | `/* */` and `//` comments | Namespace symbols are used for qualification but not emitted as standalone symbols | | Elixir | `.ex`, `.exs` | tree-sitter-elixir | class (defmodule/defimpl), type (defprotocol/@type/@callback), method (def/defp/defmacro/defguard inside module), function (top-level def) | — | `@doc`/`@moduledoc` strings | Homoiconic grammar; custom walker required. `defstruct`, `use`, `import`, `alias` not indexed | +| Ruby | `.rb`, `.rake` | tree-sitter-ruby | class, type (module), method (instance + `self.` singleton), function (top-level def) | — | `#` preceding comments | `attr_accessor`, constants, and `include`/`extend` not indexed | \* `.h` uses C++ parsing first, then falls back to C when no C++ symbols are extracted. diff --git a/README.md b/README.md index a01c763..c72080a 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,7 @@ Every tool response includes a `_meta` envelope with timing, token savings, and | C | `.c` | function, type, constant | | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.h`* | function, class, method, type, constant | | Elixir | `.ex`, `.exs` | class (module/impl), type (protocol/@type/@callback), method, function | +| Ruby | `.rb`, `.rake` | class, type (module), method, function | \* `.h` is parsed as C++ first, then falls back to C when no C++ symbols are extracted. diff --git a/pyproject.toml b/pyproject.toml index e541a42..44fb22f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jcodemunch-mcp" -version = "0.2.19" +version = "0.2.20" description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing" readme = "README.md" requires-python = ">=3.10" diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index 80f5cd7..bab04e6 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -71,6 +71,8 @@ class LanguageSpec: ".swift": "swift", ".ex": "elixir", ".exs": "elixir", + ".rb": "ruby", + ".rake": "ruby", } @@ -486,6 +488,34 @@ class LanguageSpec: ) +# Ruby specification +RUBY_SPEC = LanguageSpec( + ts_language="ruby", + symbol_node_types={ + "method": "function", # top-level → function; inside class/module → method + "singleton_method": "function", # def self.foo → always has class parent → method + "class": "class", + "module": "type", + }, + name_fields={ + "method": "name", + "singleton_method": "name", + "class": "name", + "module": "name", + }, + param_fields={ + "method": "parameters", + "singleton_method": "parameters", + }, + return_type_fields={}, + docstring_strategy="preceding_comment", + decorator_node_type=None, + container_node_types=["class", "module"], + constant_patterns=[], + type_patterns=["module"], +) + + # Language registry LANGUAGE_REGISTRY = { "python": PYTHON_SPEC, @@ -501,4 +531,5 @@ class LanguageSpec: "swift": SWIFT_SPEC, "cpp": CPP_SPEC, "elixir": ELIXIR_SPEC, + "ruby": RUBY_SPEC, } diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 51a8733..365dda4 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -213,7 +213,7 @@ async def list_tools() -> list[Tool]: "language": { "type": "string", "description": "Optional filter by language", - "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "cpp", "swift", "elixir"] + "enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart", "csharp", "c", "cpp", "swift", "elixir", "ruby"] }, "max_results": { "type": "integer", diff --git a/tests/fixtures/ruby/sample.rb b/tests/fixtures/ruby/sample.rb new file mode 100644 index 0000000..a2d9ff9 --- /dev/null +++ b/tests/fixtures/ruby/sample.rb @@ -0,0 +1,46 @@ +# Serialization helpers shared across models. +module Serializable + def serialize + instance_variables.each_with_object({}) do |var, hash| + hash[var.to_s.delete('@')] = instance_variable_get(var) + end + end +end + +# Represents a user in the system. +class User + include Serializable + + ROLES = [:admin, :moderator, :user].freeze + + attr_accessor :name, :email + + # Initializes a new User. + def initialize(name, email) + @name = name + @email = email + @role = :user + end + + # Finds a user by ID. + def self.find(id) + nil + end + + # Returns a greeting string. + def greet + "Hello, #{@name}!" + end + + private + + # Validates email format. + def valid_email? + @email.include?('@') + end +end + +# Standalone helper function. +def format_name(first, last) + "#{first} #{last}" +end diff --git a/tests/test_hardening.py b/tests/test_hardening.py index c1f9724..9779bb0 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -450,6 +450,41 @@ def test_elixir_qualified_names(self): add = _by_name(symbols, "add") assert add.qualified_name == "MyApp.Calculator.add" + # -- Ruby ------------------------------------------------------------ + + def test_ruby_class(self): + content, fname = _fixture("ruby", "sample.rb") + symbols = parse_file(content, fname, "ruby") + cls = _by_name(symbols, "User") + assert cls.kind == "class" + assert cls.language == "ruby" + + def test_ruby_module(self): + content, fname = _fixture("ruby", "sample.rb") + symbols = parse_file(content, fname, "ruby") + mod = _by_name(symbols, "Serializable") + assert mod.kind == "type" + + def test_ruby_method_qualified_name(self): + content, fname = _fixture("ruby", "sample.rb") + symbols = parse_file(content, fname, "ruby") + m = _by_name(symbols, "initialize") + assert m.qualified_name == "User.initialize" + assert m.kind == "method" + + def test_ruby_singleton_method(self): + content, fname = _fixture("ruby", "sample.rb") + symbols = parse_file(content, fname, "ruby") + find = _by_name(symbols, "find") + assert find.kind == "method" + assert find.qualified_name == "User.find" + + def test_ruby_top_level_function(self): + content, fname = _fixture("ruby", "sample.rb") + symbols = parse_file(content, fname, "ruby") + fmt = _by_name(symbols, "format_name") + assert fmt.kind == "function" + # =========================================================================== # 2. Overload Disambiguation @@ -537,6 +572,7 @@ class TestDeterminism: ("c", "sample.c"), ("cpp", "sample.cpp"), ("elixir", "sample.ex"), + ("ruby", "sample.rb"), ]) def test_deterministic_ids_and_hashes(self, language, filename): content, fname = _fixture(language, filename) diff --git a/tests/test_languages.py b/tests/test_languages.py index 5e5aef7..be813b0 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -965,3 +965,92 @@ def test_parse_cpp_header_with_cpp_keywords_stays_cpp(): assert "succ" in names +RUBY_SOURCE = '''\ +# Serialization helpers. +module Serializable + def serialize + {} + end +end + +# Represents a user. +class User + include Serializable + + def initialize(name, email) + @name = name + @email = email + end + + # Finds a user by ID. + def self.find(id) + nil + end + + def greet + "Hello, #{@name}!" + end + + private + + def valid_email? + @email.include?('@') + end +end + +# Top-level helper. +def format_name(first, last) + "#{first} #{last}" +end +''' + + +def test_parse_ruby(): + """Test Ruby parsing.""" + symbols = parse_file(RUBY_SOURCE, "sample.rb", "ruby") + + # Module → type + mod = next((s for s in symbols if s.name == "Serializable"), None) + assert mod is not None + assert mod.kind == "type" + assert "Serialization" in mod.docstring + + # Method inside module + serialize = next((s for s in symbols if s.name == "serialize"), None) + assert serialize is not None + assert serialize.kind == "method" + assert serialize.qualified_name == "Serializable.serialize" + + # Class + cls = next((s for s in symbols if s.name == "User"), None) + assert cls is not None + assert cls.kind == "class" + assert "Represents" in cls.docstring + + # Instance method + init = next((s for s in symbols if s.name == "initialize"), None) + assert init is not None + assert init.kind == "method" + assert init.qualified_name == "User.initialize" + assert init.parent == cls.id + + # Singleton method (def self.find) + find = next((s for s in symbols if s.name == "find"), None) + assert find is not None + assert find.kind == "method" + assert find.qualified_name == "User.find" + assert "Finds a user" in find.docstring + + # Private method + valid = next((s for s in symbols if s.name == "valid_email?"), None) + assert valid is not None + assert valid.kind == "method" + + # Top-level function + fmt = next((s for s in symbols if s.name == "format_name"), None) + assert fmt is not None + assert fmt.kind == "function" + assert fmt.qualified_name == "format_name" + assert "Top-level" in fmt.docstring + + From 5b02976adc02099965758196f937212eb4c190b5 Mon Sep 17 00:00:00 2001 From: gebeer Date: Fri, 6 Mar 2026 20:06:33 +0700 Subject: [PATCH 20/29] fix: improve get_file_outline tool description and error handling - Add `file_path` arg name to tool description so models don't confuse it with `file` (observed: models called the tool with `file=...`, triggering a silent KeyError that returned only `"'file_path'"`) - Catch KeyError before generic Exception to return a meaningful "Missing required argument" message for all tools Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index 6ebb6fd..50d7603 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -123,7 +123,7 @@ async def list_tools() -> list[Tool]: ), Tool( name="get_file_outline", - description="Get all symbols (functions, classes, methods) in a file with signatures and summaries.", + description="Get all symbols (functions, classes, methods) in a file with signatures and summaries. Pass repo and file_path (e.g. 'src/main.py').", inputSchema={ "type": "object", "properties": { @@ -365,6 +365,8 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return [TextContent(type="text", text=json.dumps(result, indent=2))] + except KeyError as e: + return [TextContent(type="text", text=json.dumps({"error": f"Missing required argument: {e}. Check the tool schema for correct parameter names."}, indent=2))] except Exception as e: return [TextContent(type="text", text=json.dumps({"error": str(e)}, indent=2))] From 706281f78d7637933ea79514b698a30cf1552643 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 07:07:43 -0600 Subject: [PATCH 21/29] fix(docs): add Perl to LANGUAGES.md - Add Perl row to LANGUAGE_SUPPORT.md feature matrix - Documents extensions (.pl, .pm, .t), symbol types (function, class, constant), and docstring extraction (preceding # comments and POD blocks) Co-Authored-By: Claude Sonnet 4.6 --- LANGUAGE_SUPPORT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index ba1c9b7..49c9dda 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -14,6 +14,7 @@ | Dart | `.dart` | tree-sitter-dart | function, class (class/mixin/extension), method, type (enum/typedef) | `@annotation` | `///` doc comments | Constructors and top-level constants are not indexed | | C# | `.cs` | tree-sitter-csharp | class (class/record), method (method/constructor), type (interface/enum/struct/delegate) | `[Attribute]` | `/// ` XML doc comments | Properties and `const` fields not indexed | | C | `.c`, `.h` | tree-sitter-c | function, type (struct/enum/union), constant | — | `/* */` and `//` comments | `#define` macros extracted as constants; no class/method hierarchy | +| Perl | `.pl`, `.pm`, `.t` | tree-sitter-perl | function, class (package), constant | — | Preceding `#` comments and POD blocks | `use constant` extracted as constants; no return type inference | --- From 3d1105e8e6448bfd50c46982d86c25621414c8ab Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 07:50:38 -0600 Subject: [PATCH 22/29] feat(parser): add JCODEMUNCH_EXTRA_EXTENSIONS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add _apply_extra_extensions() to languages.py, called at module load time after LANGUAGE_REGISTRY. Reads comma-separated .ext:lang pairs from JCODEMUNCH_EXTRA_EXTENSIONS env var and merges valid entries into LANGUAGE_EXTENSIONS in-place. Unknown languages and malformed entries are skipped with WARNING logs. Import chain (index_folder.py, index_repo.py) is unaffected — both import the same LANGUAGE_EXTENSIONS dict object, so in-place mutation propagates automatically. Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/parser/languages.py | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/jcodemunch_mcp/parser/languages.py b/src/jcodemunch_mcp/parser/languages.py index c8c49ff..bf9c33b 100644 --- a/src/jcodemunch_mcp/parser/languages.py +++ b/src/jcodemunch_mcp/parser/languages.py @@ -1,5 +1,7 @@ """Language registry with LanguageSpec definitions for all supported languages.""" +import logging +import os from dataclasses import dataclass from typing import Optional @@ -425,3 +427,53 @@ class LanguageSpec: "c": C_SPEC, "perl": PERL_SPEC, } + +logger = logging.getLogger(__name__) + + +def _apply_extra_extensions() -> None: + """Merge JCODEMUNCH_EXTRA_EXTENSIONS env var into LANGUAGE_EXTENSIONS. + + Format: comma-separated `.ext:lang` pairs, e.g. `.cgi:perl,.psgi:perl`. + - Unknown language values (not in LANGUAGE_REGISTRY) are skipped with a WARNING. + - Malformed entries (missing colon, empty ext or lang) are skipped with a WARNING. + - Valid entries add or override entries in LANGUAGE_EXTENSIONS. + """ + raw = os.environ.get("JCODEMUNCH_EXTRA_EXTENSIONS", "").strip() + if not raw: + return + for token in raw.split(","): + token = token.strip() + if not token: + continue + if ":" not in token: + logger.warning( + "JCODEMUNCH_EXTRA_EXTENSIONS: malformed entry %r (expected .ext:lang) — skipped", + token, + ) + continue + ext, _, lang = token.partition(":") + ext = ext.strip() + lang = lang.strip() + if not ext or not lang: + logger.warning( + "JCODEMUNCH_EXTRA_EXTENSIONS: malformed entry %r (empty ext or lang) — skipped", + token, + ) + continue + if lang not in LANGUAGE_REGISTRY: + logger.warning( + "JCODEMUNCH_EXTRA_EXTENSIONS: unknown language %r in entry %r — skipped", + lang, + token, + ) + continue + LANGUAGE_EXTENSIONS[ext] = lang + logger.debug( + "JCODEMUNCH_EXTRA_EXTENSIONS: registered %r → %r", + ext, + lang, + ) + + +_apply_extra_extensions() From 63fe59613cc500359d8501ad785184b9f4523aee Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 07:50:45 -0600 Subject: [PATCH 23/29] feat(docs): add JCODEMUNCH_EXTRA_EXTENSIONS startup log and documentation Log extra extension mappings at INFO level in server.py startup. Document the env var format, rules, and registered languages in LANGUAGE_SUPPORT.md. Co-Authored-By: Claude Sonnet 4.6 --- LANGUAGE_SUPPORT.md | 23 +++++++++++++++++++++++ src/jcodemunch_mcp/server.py | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md index 49c9dda..4a538df 100644 --- a/LANGUAGE_SUPPORT.md +++ b/LANGUAGE_SUPPORT.md @@ -106,3 +106,26 @@ print_tree(tree.root_node) ``` This inspection process helps identify the correct `symbol_node_types`, `name_fields`, and extraction rules when adding support for a new language. + +--- + +## Configuration + +### Custom Extension Mappings (`JCODEMUNCH_EXTRA_EXTENSIONS`) + +Set the `JCODEMUNCH_EXTRA_EXTENSIONS` environment variable to add or override file extension → language mappings without modifying source code. + +**Format:** comma-separated `.ext:lang` pairs + +``` +JCODEMUNCH_EXTRA_EXTENSIONS=".cgi:perl,.psgi:perl,.pl6:perl" +``` + +**Rules:** +- Extensions can add new mappings or override built-in ones. +- `lang` must be a registered language name (e.g. `perl`, `python`, `go`). Unknown language values are logged as a warning and skipped. +- Malformed entries (missing colon, empty extension or language) are logged as a warning and skipped. +- Built-in mappings are never removed — only added to or overridden. +- Mappings are applied once at import time and are visible to all indexing tools. + +**Registered language names:** `python`, `javascript`, `typescript`, `go`, `rust`, `java`, `php`, `dart`, `csharp`, `c`, `perl` diff --git a/src/jcodemunch_mcp/server.py b/src/jcodemunch_mcp/server.py index efc42d3..f57ae90 100644 --- a/src/jcodemunch_mcp/server.py +++ b/src/jcodemunch_mcp/server.py @@ -415,6 +415,13 @@ def main(argv: Optional[list[str]] = None): handlers=handlers, ) + # Log extra extension mappings if configured + _extra_exts = os.environ.get("JCODEMUNCH_EXTRA_EXTENSIONS", "").strip() + if _extra_exts: + logging.getLogger(__name__).info( + "Extra extension mappings active: %s", _extra_exts + ) + asyncio.run(run_server()) From a1c0c3d94a3afd2620c7ac99d1ce1bd1e44a67d9 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 07:52:03 -0600 Subject: [PATCH 24/29] test(parser): add test coverage for JCODEMUNCH_EXTRA_EXTENSIONS Add tests/test_extra_extensions.py with 10 test cases covering: - Valid .ext:lang pair merging - Unknown language skipping with WARNING - Malformed entries (no colon, empty ext, empty lang) with WARNING - Empty/absent env var leaving LANGUAGE_EXTENSIONS unchanged - Whitespace-only env var handling - Override of built-in extension mappings - Mixed valid and invalid entries - Whitespace stripping in entries Co-Authored-By: Claude Sonnet 4.6 --- tests/test_extra_extensions.py | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/test_extra_extensions.py diff --git a/tests/test_extra_extensions.py b/tests/test_extra_extensions.py new file mode 100644 index 0000000..80465bb --- /dev/null +++ b/tests/test_extra_extensions.py @@ -0,0 +1,110 @@ +"""Tests for JCODEMUNCH_EXTRA_EXTENSIONS env var handling.""" + +import pytest +from jcodemunch_mcp.parser.languages import ( + LANGUAGE_EXTENSIONS, + _apply_extra_extensions, +) + + +@pytest.fixture(autouse=True) +def restore_extensions(): + """Restore LANGUAGE_EXTENSIONS to its original state after each test.""" + original = dict(LANGUAGE_EXTENSIONS) + yield + LANGUAGE_EXTENSIONS.clear() + LANGUAGE_EXTENSIONS.update(original) + + +def test_valid_extra_extensions(monkeypatch): + """Valid .ext:lang pairs are merged into LANGUAGE_EXTENSIONS.""" + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ".cgi:perl,.psgi:perl") + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS[".cgi"] == "perl" + assert LANGUAGE_EXTENSIONS[".psgi"] == "perl" + + +def test_unknown_language_skipped(monkeypatch, caplog): + """Unknown language values are skipped with a WARNING log.""" + import logging + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ".xyz:cobol") + with caplog.at_level(logging.WARNING): + _apply_extra_extensions() + assert ".xyz" not in LANGUAGE_EXTENSIONS + assert any("cobol" in r.message or "cobol" in str(r.args) for r in caplog.records) + + +def test_malformed_entry_no_colon(monkeypatch, caplog): + """Entry with no colon separator is skipped with a WARNING log.""" + import logging + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ".cgiperls") + with caplog.at_level(logging.WARNING): + _apply_extra_extensions() + assert ".cgiperls" not in LANGUAGE_EXTENSIONS + assert len(caplog.records) >= 1 + + +def test_malformed_entry_empty_ext(monkeypatch, caplog): + """Entry with empty extension is skipped with a WARNING log.""" + import logging + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ":perl") + with caplog.at_level(logging.WARNING): + _apply_extra_extensions() + assert "" not in LANGUAGE_EXTENSIONS + assert len(caplog.records) >= 1 + + +def test_malformed_entry_empty_lang(monkeypatch, caplog): + """Entry with empty language is skipped with a WARNING log.""" + import logging + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ".cgi:") + with caplog.at_level(logging.WARNING): + _apply_extra_extensions() + assert ".cgi" not in LANGUAGE_EXTENSIONS + assert len(caplog.records) >= 1 + + +def test_empty_env_var(monkeypatch): + """Absent or empty env var leaves LANGUAGE_EXTENSIONS unchanged.""" + monkeypatch.delenv("JCODEMUNCH_EXTRA_EXTENSIONS", raising=False) + before = dict(LANGUAGE_EXTENSIONS) + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS == before + + +def test_whitespace_only_env_var(monkeypatch): + """Whitespace-only env var leaves LANGUAGE_EXTENSIONS unchanged.""" + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", " ") + before = dict(LANGUAGE_EXTENSIONS) + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS == before + + +def test_override_builtin_extension(monkeypatch): + """A valid entry can override an existing built-in extension mapping.""" + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", ".pl:python") + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS[".pl"] == "python" + + +def test_mixed_valid_and_invalid(monkeypatch, caplog): + """Valid entries are applied even when mixed with invalid ones.""" + import logging + monkeypatch.setenv( + "JCODEMUNCH_EXTRA_EXTENSIONS", + ".cgi:perl,.xyz:cobol,.psgi:perl,badentry" + ) + with caplog.at_level(logging.WARNING): + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS[".cgi"] == "perl" + assert LANGUAGE_EXTENSIONS[".psgi"] == "perl" + assert ".xyz" not in LANGUAGE_EXTENSIONS + assert len(caplog.records) >= 2 + + +def test_extra_whitespace_in_entries(monkeypatch): + """Leading/trailing whitespace in tokens is stripped.""" + monkeypatch.setenv("JCODEMUNCH_EXTRA_EXTENSIONS", " .cgi : perl , .psgi : perl ") + _apply_extra_extensions() + assert LANGUAGE_EXTENSIONS[".cgi"] == "perl" + assert LANGUAGE_EXTENSIONS[".psgi"] == "perl" From 357dddeea97a30514456a93c356e2e245adf598a Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 08:34:52 -0600 Subject: [PATCH 25/29] chore(repo): add uv.lock and update .gitignore for VBW planning files Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 5 + uv.lock | 1526 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1531 insertions(+) create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 568bddc..e824e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ dmypy.json # Local working notes (may contain sensitive info) prompt.md + +CLAUDE.md +.vbw-planning + +.vbw-planning/ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3a83e3f --- /dev/null +++ b/uv.lock @@ -0,0 +1,1526 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version < '3.11'", +] + +[[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, 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, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319, upload-time = "2026-03-06T06:00:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974, upload-time = "2026-03-06T06:00:28.222Z" }, + { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866, upload-time = "2026-03-06T06:00:29.769Z" }, + { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239, upload-time = "2026-03-06T06:00:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529, upload-time = "2026-03-06T06:00:32.608Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152, upload-time = "2026-03-06T06:00:33.93Z" }, + { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226, upload-time = "2026-03-06T06:00:35.469Z" }, + { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933, upload-time = "2026-03-06T06:00:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647, upload-time = "2026-03-06T06:00:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533, upload-time = "2026-03-06T06:00:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901, upload-time = "2026-03-06T06:00:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950, upload-time = "2026-03-06T06:00:45.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546, upload-time = "2026-03-06T06:00:46.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516, upload-time = "2026-03-06T06:00:47.924Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906, upload-time = "2026-03-06T06:00:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258, upload-time = "2026-03-06T06:00:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +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/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[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/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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "google-ai-generativelanguage" +version = "0.6.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version >= '3.14'" }, + { name = "google-api-core", version = "2.30.0", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version < '3.14'" }, + { name = "google-auth" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/d1/48fe5d7a43d278e9f6b5ada810b0a3530bbeac7ed7fcbcd366f932f05316/google_ai_generativelanguage-0.6.15.tar.gz", hash = "sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3", size = 1375443, upload-time = "2025-01-13T21:50:47.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a3/67b8a6ff5001a1d8864922f2d6488dc2a14367ceb651bc3f09a947f2f306/google_ai_generativelanguage-0.6.15-py3-none-any.whl", hash = "sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c", size = 1327356, upload-time = "2025-01-13T21:50:44.174Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/d8/894716a5423933f5c8d2d5f04b16f052a515f78e815dab0c2c6f1fd105dc/google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7", size = 162489, upload-time = "2025-10-03T00:07:32.924Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio", marker = "python_full_version >= '3.14'" }, + { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, + { name = "proto-plus", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio", marker = "python_full_version < '3.14'" }, + { name = "grpcio-status", marker = "python_full_version < '3.14'" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.192.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-api-core", version = "2.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/d8/489052a40935e45b9b5b3d6accc14b041360c1507bdc659c2e1a19aaa3ff/google_api_python_client-2.192.0.tar.gz", hash = "sha256:d48cfa6078fadea788425481b007af33fe0ab6537b78f37da914fb6fc112eb27", size = 14209505, upload-time = "2026-03-05T15:17:01.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/76/ec4128f00fefb9011635ae2abc67d7dacd05c8559378f8f05f0c907c38d8/google_api_python_client-2.192.0-py3-none-any.whl", hash = "sha256:63a57d4457cd97df1d63eb89c5fda03c5a50588dcbc32c0115dd1433c08f4b62", size = 14783267, upload-time = "2026-03-05T15:16:58.804Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, +] + +[[package]] +name = "google-generativeai" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-ai-generativelanguage" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-api-core", version = "2.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-api-python-client" }, + { name = "google-auth" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/0f/ef33b5bb71437966590c6297104c81051feae95d54b11ece08533ef937d3/google_generativeai-0.8.6-py3-none-any.whl", hash = "sha256:37a0eaaa95e5bbf888828e20a4a1b2c196cc9527d194706e58a68ff388aeb0fa", size = 155098, upload-time = "2025-12-16T17:53:58.61Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.71.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677, upload-time = "2025-06-28T04:24:05.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" }, +] + +[[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, 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, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +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, 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, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { 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, 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, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jcodemunch-mcp" +version = "0.2.13" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "mcp" }, + { name = "pathspec" }, + { name = "tree-sitter-language-pack" }, +] + +[package.optional-dependencies] +all = [ + { name = "anthropic" }, + { name = "google-generativeai" }, +] +anthropic = [ + { name = "anthropic" }, +] +gemini = [ + { name = "google-generativeai" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", marker = "extra == 'all'", specifier = ">=0.40.0" }, + { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.40.0" }, + { name = "google-generativeai", marker = "extra == 'all'", specifier = ">=0.8.0" }, + { name = "google-generativeai", marker = "extra == 'gemini'", specifier = ">=0.8.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "mcp", specifier = ">=1.0.0,<1.10.0" }, + { name = "pathspec", specifier = ">=0.12.0" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-asyncio", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "tree-sitter-language-pack", specifier = ">=0.7.0,<1.0.0" }, +] +provides-extras = ["anthropic", "gemini", "all", "test"] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "mcp" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294, upload-time = "2025-06-12T08:20:30.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[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, 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, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/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]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[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]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[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, 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, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d4/f7ffb855cb039b7568aba4911fbe42e4c39c0e4398387c8e0d8251489992/tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7", size = 146749, upload-time = "2025-09-25T17:37:16.475Z" }, + { url = "https://files.pythonhosted.org/packages/9a/58/f8a107f9f89700c0ab2930f1315e63bdedccbb5fd1b10fcbc5ebadd54ac8/tree_sitter-0.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44488e0e78146f87baaa009736886516779253d6d6bac3ef636ede72bc6a8234", size = 137766, upload-time = "2025-09-25T17:37:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/357158d39f01699faea466e8fd5a849f5a30252c68414bddc20357a9ac79/tree_sitter-0.25.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2f8e7d6b2f8489d4a9885e3adcaef4bc5ff0a275acd990f120e29c4ab3395c5", size = 599809, upload-time = "2025-09-25T17:37:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/68ae301626f2393a62119481cb660eb93504a524fc741a6f1528a4568cf6/tree_sitter-0.25.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b570690f87f1da424cd690e51cc56728d21d63f4abd4b326d382a30353acc7", size = 627676, upload-time = "2025-09-25T17:37:20.715Z" }, + { url = "https://files.pythonhosted.org/packages/69/fe/4c1bef37db5ca8b17ca0b3070f2dff509468a50b3af18f17665adcab42b9/tree_sitter-0.25.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0ec41b895da717bc218a42a3a7a0bfcfe9a213d7afaa4255353901e0e21f696", size = 624281, upload-time = "2025-09-25T17:37:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/3283cb7fa251cae2a0bf8661658021a789810db3ab1b0569482d4a3671fd/tree_sitter-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:7712335855b2307a21ae86efe949c76be36c6068d76df34faa27ce9ee40ff444", size = 127295, upload-time = "2025-09-25T17:37:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/ceb05e6de281aebe82b68662890619580d4ffe09283ebd2ceabcf5df7b4a/tree_sitter-0.25.2-cp310-cp310-win_arm64.whl", hash = "sha256:a925364eb7fbb9cdce55a9868f7525a1905af512a559303bd54ef468fd88cb37", size = 113991, upload-time = "2025-09-25T17:37:23.854Z" }, + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, + { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/ca/72/fc6846795bcdae2f8aa94cc8b1d1af33d634e08be63e294ff0d6794b1efc/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8024e466b2f5611c6dc90321f232d8584893c7fb88b75e4a831992f877616d2", size = 402830, upload-time = "2024-11-11T05:25:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3a/b6028c5890ce6653807d5fa88c72232c027c6ceb480dbeb3b186d60e5971/tree_sitter_c_sharp-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7f9bf876866835492281d336b9e1f9626ab668737f74e914c31d285261507da7", size = 397880, upload-time = "2024-11-11T05:25:25.937Z" }, + { url = "https://files.pythonhosted.org/packages/47/d2/4facaa34b40f8104d8751746d0e1cd2ddf0beb9f1404b736b97f372bd1f3/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:ae9a9e859e8f44e2b07578d44f9a220d3fa25b688966708af6aa55d42abeebb3", size = 377562, upload-time = "2024-11-11T05:25:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/d8/88/3cf6bd9959d94d1fec1e6a9c530c5f08ff4115a474f62aedb5fedb0f7241/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:c81548347a93347be4f48cb63ec7d60ef4b0efa91313330e69641e49aa5a08c5", size = 375157, upload-time = "2024-11-11T05:25:30.839Z" }, +] + +[[package]] +name = "tree-sitter-embedded-template" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/a7/77729fefab8b1b5690cfc54328f2f629d1c076d16daf32c96ba39d3a3a3a/tree_sitter_embedded_template-0.25.0.tar.gz", hash = "sha256:7d72d5e8a1d1d501a7c90e841b51f1449a90cc240be050e4fb85c22dab991d50", size = 14114, upload-time = "2025-08-29T00:42:51.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/9d/3e3c8ee0c019d3bace728300a1ca807c03df39e66cc51e9a5e7c9d1e1909/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fa0d06467199aeb33fb3d6fa0665bf9b7d5a32621ffdaf37fd8249f8a8050649", size = 10266, upload-time = "2025-08-29T00:42:44.148Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ab/6d4e43b736b2a895d13baea3791dc8ce7245bedf4677df9e7deb22e23a2a/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc7aacbc2985a5d7e7fe7334f44dffe24c38fb0a8295c4188a04cf21a3d64a73", size = 10650, upload-time = "2025-08-29T00:42:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/9f/97/ea3d1ea4b320fe66e0468b9f6602966e544c9fe641882484f9105e50ee0c/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7c88c3dd8b94b3c9efe8ae071ff6b1b936a27ac5f6e651845c3b9631fa4c1c2", size = 18268, upload-time = "2025-08-29T00:42:46.03Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/0f42ca894a8f7c298cf336080046ccc14c10e8f4ea46d455f640193181b2/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:025f7ca84218dcd8455efc901bdbcc2689fb694f3a636c0448e322a23d4bc96b", size = 19068, upload-time = "2025-08-29T00:42:46.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/0b720bcae7c2dd0a44889c09e800a2f8eb08c496dede9f2b97683506c4c3/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b5dc1aef6ffa3fae621fe037d85dd98948b597afba20df29d779c426be813ee5", size = 18518, upload-time = "2025-08-29T00:42:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/14/8a/d745071afa5e8bdf5b381cf84c4dc6be6c79dee6af8e0ff07476c3d8e4aa/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d0a35cfe634c44981a516243bc039874580e02a2990669313730187ce83a5bc6", size = 18267, upload-time = "2025-08-29T00:42:48.635Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/728355e594fca140f793f234fdfec195366b6956b35754d00ea97ca18b21/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:3e05a4ac013d54505e75ae48e1a0e9db9aab19949fe15d9f4c7345b11a84a069", size = 13049, upload-time = "2025-08-29T00:42:49.589Z" }, + { url = "https://files.pythonhosted.org/packages/d8/de/afac475e694d0e626b0808f3c86339c349cd15c5163a6a16a53cc11cf892/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:2751d402179ac0e83f2065b249d8fe6df0718153f1636bcb6a02bde3e5730db9", size = 11978, upload-time = "2025-08-29T00:42:50.226Z" }, +] + +[[package]] +name = "tree-sitter-language-pack" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tree-sitter" }, + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-embedded-template" }, + { name = "tree-sitter-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/83/d1bc738d6f253f415ee54a8afb99640f47028871436f53f2af637c392c4f/tree_sitter_language_pack-0.13.0.tar.gz", hash = "sha256:032034c5e27b1f6e00730b9e7c2dbc8203b4700d0c681fd019d6defcf61183ec", size = 51353370, upload-time = "2025-11-26T14:01:04.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/38/aec1f450ae5c4796de8345442f297fcf8912c7d2e00a66d3236ff0f825ed/tree_sitter_language_pack-0.13.0-cp310-abi3-macosx_10_15_universal2.whl", hash = "sha256:0e7eae812b40a2dc8a12eb2f5c55e130eb892706a0bee06215dd76affeb00d07", size = 32991857, upload-time = "2025-11-26T14:00:51.459Z" }, + { url = "https://files.pythonhosted.org/packages/90/09/11f51c59ede786dccddd2d348d5d24a1d99c54117d00f88b477f5fae4bd5/tree_sitter_language_pack-0.13.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:7fdacf383418a845b20772118fcb53ad245f9c5d409bd07dae16acec65151756", size = 20092989, upload-time = "2025-11-26T14:00:54.202Z" }, + { url = "https://files.pythonhosted.org/packages/72/9d/644db031047ab1a70fc5cb6a79a4d4067080fac628375b2320752d2d7b58/tree_sitter_language_pack-0.13.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:0d4f261fce387ae040dae7e4d1c1aca63d84c88320afcc0961c123bec0be8377", size = 19952029, upload-time = "2025-11-26T14:00:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/92/5fd749bbb3f5e4538492c77de7bc51a5e479fec6209464ddc25be9153b13/tree_sitter_language_pack-0.13.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:78f369dc4d456c5b08d659939e662c2f9b9fba8c0ec5538a1f973e01edfcf04d", size = 19944614, upload-time = "2025-11-26T14:00:59.381Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/2287f07723c063475d6657babed0d5569f4b499e393ab51354d529c3e7b5/tree_sitter_language_pack-0.13.0-cp310-abi3-win_amd64.whl", hash = "sha256:1cdbc88a03dacd47bec69e56cc20c48eace1fbb6f01371e89c3ee6a2e8f34db1", size = 16896852, upload-time = "2025-11-26T14:01:01.788Z" }, +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/b6/941d356ac70c90b9d2927375259e3a4204f38f7499ec6e7e8a95b9664689/tree_sitter_yaml-0.7.2.tar.gz", hash = "sha256:756db4c09c9d9e97c81699e8f941cb8ce4e51104927f6090eefe638ee567d32c", size = 84882, upload-time = "2025-10-07T14:40:36.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/29/c0b8dbff302c49ff4284666ffb6f2f21145006843bb4c3a9a85d0ec0b7ae/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7e269ddcfcab8edb14fbb1f1d34eed1e1e26888f78f94eedfe7cc98c60f8bc9f", size = 43898, upload-time = "2025-10-07T14:40:29.486Z" }, + { url = "https://files.pythonhosted.org/packages/18/0d/15a5add06b3932b5e4ce5f5e8e179197097decfe82a0ef000952c8b98216/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0807b7966e23ddf7dddc4545216e28b5a58cdadedcecca86b8d8c74271a07870", size = 44691, upload-time = "2025-10-07T14:40:30.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/92/c4b896c90d08deb8308fadbad2210fdcc4c66c44ab4292eac4e80acb4b61/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1a5c60c98b6c4c037aae023569f020d0c489fad8dc26fdfd5510363c9c29a41", size = 91430, upload-time = "2025-10-07T14:40:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/61f1fed31eb6d46ff080b8c0d53658cf29e10263f41ef5fe34768908037a/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88636d19d0654fd24f4f242eaaafa90f6f5ebdba8a62e4b32d251ed156c51a2a", size = 92428, upload-time = "2025-10-07T14:40:31.954Z" }, + { url = "https://files.pythonhosted.org/packages/e3/62/a33a04d19b7f9a0ded780b9c9fcc6279e37c5d00b89b00425bb807a22cc2/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d2e8f0bb14aa4537320952d0f9607eef3021d5aada8383c34ebeece17db1e06", size = 90580, upload-time = "2025-10-07T14:40:33.037Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e7/9525defa7b30792623f56b1fba9bbba361752348875b165b8975b87398fd/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74ca712c50fc9d7dbc68cb36b4a7811d6e67a5466b5a789f19bf8dd6084ef752", size = 90455, upload-time = "2025-10-07T14:40:33.778Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d6/8d1e1ace03db3b02e64e91daf21d1347941d1bbecc606a5473a1a605250d/tree_sitter_yaml-0.7.2-cp310-abi3-win_amd64.whl", hash = "sha256:7587b5ca00fc4f9a548eff649697a3b395370b2304b399ceefa2087d8a6c9186", size = 45514, upload-time = "2025-10-07T14:40:34.562Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/dcf3ea1c4f5da9b10353b9af4455d756c92d728a8f58f03c480d3ef0ead5/tree_sitter_yaml-0.7.2-cp310-abi3-win_arm64.whl", hash = "sha256:f63c227b18e7ce7587bce124578f0bbf1f890ac63d3e3cd027417574273642c4", size = 44065, upload-time = "2025-10-07T14:40:35.337Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.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/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] From 72028b80bb3b8ce5d5c1dca809135c5baa5e1e66 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 08:51:02 -0600 Subject: [PATCH 26/29] fix(indexer): raise file cap to 10k and fix SKIP_PATTERNS false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Raise discover_local_files() default max_files from 500 → 10,000 so large monorepos are fully indexed instead of silently truncated - Fix should_skip_file() in index_folder.py and index_repo.py to use segment-aware matching for directory patterns (those ending in "/") preventing false positives on names like "rebuild/", "proto-utils/", or "build-tools/" that contain a skip pattern as a substring - Update misleading result note to reflect new 10,000 threshold Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/tools/index_folder.py | 17 +++++++++++------ src/jcodemunch_mcp/tools/index_repo.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/jcodemunch_mcp/tools/index_folder.py b/src/jcodemunch_mcp/tools/index_folder.py index 46cc83e..74474d5 100644 --- a/src/jcodemunch_mcp/tools/index_folder.py +++ b/src/jcodemunch_mcp/tools/index_folder.py @@ -38,11 +38,16 @@ def should_skip_file(path: str) -> bool: """Check if file should be skipped based on path patterns.""" - # Normalize path separators for matching normalized = path.replace("\\", "/") for pattern in SKIP_PATTERNS: - if pattern in normalized: - return True + if pattern.endswith("/"): + # Directory pattern: match only complete path segments to avoid + # false positives on names like "rebuild/" or "proto-utils/" + if normalized.startswith(pattern) or ("/" + pattern) in normalized: + return True + else: + if pattern in normalized: + return True return False @@ -60,7 +65,7 @@ def _load_gitignore(folder_path: Path) -> Optional[pathspec.PathSpec]: def discover_local_files( folder_path: Path, - max_files: int = 500, + max_files: int = 10_000, max_size: int = DEFAULT_MAX_FILE_SIZE, extra_ignore_patterns: Optional[list[str]] = None, follow_symlinks: bool = False, @@ -430,8 +435,8 @@ def index_folder( if warnings: result["warnings"] = warnings - if len(source_files) >= 500: - result["note"] = "Folder has many files; indexed first 500" + if len(source_files) >= 10_000: + result["note"] = "Large repository: indexed first 10,000 source files (priority: src/, lib/, pkg/, cmd/, internal/)" return result diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index c386abd..a8d2975 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -77,9 +77,16 @@ async def fetch_repo_tree(owner: str, repo: str, token: Optional[str] = None) -> def should_skip_file(path: str) -> bool: """Check if file should be skipped based on path patterns.""" + normalized = path.replace("\\", "/") for pattern in SKIP_PATTERNS: - if pattern in path: - return True + if pattern.endswith("/"): + # Directory pattern: match only complete path segments to avoid + # false positives on names like "rebuild/" or "proto-utils/" + if normalized.startswith(pattern) or ("/" + pattern) in normalized: + return True + else: + if pattern in normalized: + return True return False From 6aea8712644d3c9d3fab5b02800886e02f6bb2a8 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 08:58:58 -0600 Subject: [PATCH 27/29] fix(parser): address QA round 1 findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip # comment markers in _clean_comment_markers() for Perl/shell/Python - Clean POD directives (=pod, =head1, =cut etc.) leaving only content text - Fix //! prefix ordering (must check before //) in comment marker stripping - Raise index_repo max_files from 500 → 2,000 (lower than local 10k due to per-file GitHub API calls); update truncation warning message to match Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/parser/extractor.py | 29 ++++++++++++++++++-------- src/jcodemunch_mcp/tools/index_repo.py | 6 +++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/jcodemunch_mcp/parser/extractor.py b/src/jcodemunch_mcp/parser/extractor.py index 751f45b..7dae9ac 100644 --- a/src/jcodemunch_mcp/parser/extractor.py +++ b/src/jcodemunch_mcp/parser/extractor.py @@ -294,31 +294,42 @@ def _extract_preceding_comments(node, source_bytes: bytes) -> str: def _clean_comment_markers(text: str) -> str: """Clean comment markers from docstring.""" + # POD block: strip directive lines (=pod, =head1, =cut, etc.), keep content + if text.lstrip().startswith("="): + content_lines = [] + for line in text.split("\n"): + stripped = line.strip() + if stripped.startswith("="): + continue + content_lines.append(stripped) + return "\n".join(content_lines).strip() + lines = text.split("\n") cleaned = [] - for line in lines: line = line.strip() - # Remove leading comment markers + # Remove leading comment markers (order matters: longer prefixes first) if line.startswith("/**"): line = line[3:] - elif line.startswith("/*"): - line = line[2:] + elif line.startswith("//!"): + line = line[3:] elif line.startswith("///"): line = line[3:] elif line.startswith("//"): line = line[2:] - elif line.startswith("//!"): - line = line[3:] + elif line.startswith("/*"): + line = line[2:] elif line.startswith("*"): line = line[1:] - + elif line.startswith("#"): + line = line[1:] + # Remove trailing */ if line.endswith("*/"): line = line[:-2] - + cleaned.append(line.strip()) - + return "\n".join(cleaned).strip() diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index a8d2975..21d0749 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -93,7 +93,7 @@ def should_skip_file(path: str) -> bool: def discover_source_files( tree_entries: list[dict], gitignore_content: Optional[str] = None, - max_files: int = 500, + max_files: int = 2000, # Lower than local (10k) due to per-file GitHub API calls max_size: int = 500 * 1024 # 500KB ) -> list[str]: """Discover source files from tree entries. @@ -388,8 +388,8 @@ async def fetch_with_limit(path: str) -> tuple[str, str]: if warnings: result["warnings"] = warnings - if len(source_files) >= 500: - result["warnings"] = warnings + ["Repository has many files; indexed first 500"] + if len(source_files) >= 2000: + result["warnings"] = warnings + ["Repository has many files; indexed first 2,000 (priority: src/, lib/, pkg/, cmd/, internal/)"] return result From ece87b0ab423e06491719e8e89dcf4daa44fe7b9 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 09:10:29 -0600 Subject: [PATCH 28/29] fix(indexer): restore segment-aware SKIP_PATTERNS fix lost in merge The should_skip_file() segment-aware matching (from commit 72028b8) was accidentally reverted when resolving merge conflicts with --theirs. Reapplied to both index_folder.py and index_repo.py. Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/tools/index_folder.py | 11 ++++++++--- src/jcodemunch_mcp/tools/index_repo.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/jcodemunch_mcp/tools/index_folder.py b/src/jcodemunch_mcp/tools/index_folder.py index 3ec6843..86f5c66 100644 --- a/src/jcodemunch_mcp/tools/index_folder.py +++ b/src/jcodemunch_mcp/tools/index_folder.py @@ -43,11 +43,16 @@ def should_skip_file(path: str) -> bool: """Check if file should be skipped based on path patterns.""" - # Normalize path separators for matching normalized = path.replace("\\", "/") for pattern in SKIP_PATTERNS: - if pattern in normalized: - return True + if pattern.endswith("/"): + # Directory pattern: match only complete path segments to avoid + # false positives on names like "rebuild/" or "proto-utils/" + if normalized.startswith(pattern) or ("/" + pattern) in normalized: + return True + else: + if pattern in normalized: + return True return False diff --git a/src/jcodemunch_mcp/tools/index_repo.py b/src/jcodemunch_mcp/tools/index_repo.py index bf965d3..78043f5 100644 --- a/src/jcodemunch_mcp/tools/index_repo.py +++ b/src/jcodemunch_mcp/tools/index_repo.py @@ -80,9 +80,16 @@ async def fetch_repo_tree(owner: str, repo: str, token: Optional[str] = None) -> def should_skip_file(path: str) -> bool: """Check if file should be skipped based on path patterns.""" + normalized = path.replace("\\", "/") for pattern in SKIP_PATTERNS: - if pattern in path: - return True + if pattern.endswith("/"): + # Directory pattern: match only complete path segments to avoid + # false positives on names like "rebuild/" or "proto-utils/" + if normalized.startswith(pattern) or ("/" + pattern) in normalized: + return True + else: + if pattern in normalized: + return True return False From 3a3c515aa398f13398c3079c4cfef2f8f9b67872 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Fri, 6 Mar 2026 09:14:27 -0600 Subject: [PATCH 29/29] fix(security): raise DEFAULT_MAX_INDEX_FILES from 500 to 10,000 The upstream refactor introduced get_max_index_files() with a 500-file default. Monorepos easily exceed this cap, causing silent truncation. Raise the default to 10,000 to match prior behavior; users can still override via JCODEMUNCH_MAX_INDEX_FILES env var. Co-Authored-By: Claude Sonnet 4.6 --- src/jcodemunch_mcp/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jcodemunch_mcp/security.py b/src/jcodemunch_mcp/security.py index 2f071a4..dde21e8 100644 --- a/src/jcodemunch_mcp/security.py +++ b/src/jcodemunch_mcp/security.py @@ -206,7 +206,7 @@ def safe_decode(data: bytes, encoding: str = "utf-8") -> str: # --- Composite Filters --- DEFAULT_MAX_FILE_SIZE = 500 * 1024 # 500KB -DEFAULT_MAX_INDEX_FILES = 500 +DEFAULT_MAX_INDEX_FILES = 10_000 MAX_INDEX_FILES_ENV_VAR = "JCODEMUNCH_MAX_INDEX_FILES"