From 84f29e690aedb8498db2491a1dc7854b7134339f Mon Sep 17 00:00:00 2001 From: indrajeet0510 Date: Mon, 8 Jun 2026 05:13:49 +0530 Subject: [PATCH] feat: add DSL output format for graph tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opt-in format="dsl" parameter on get_impact_radius_tool, query_graph_tool, and get_review_context_tool returns nodes and edges as one-line strings instead of JSON dicts. Per-row: ~4x nodes, ~2.5x edges. Full-response on a 71-node blast radius: ~8,000 tokens saved per call. Default stays "dict" — fully backwards compatible. --- README.md | 7 + code_review_graph/graph.py | 144 +++++++++++ code_review_graph/main.py | 10 +- code_review_graph/tools/query.py | 125 ++++++---- code_review_graph/tools/review.py | 16 +- pyproject.toml | 1 + scripts/bench_dsl_output.py | 217 +++++++++++++++++ tests/test_dsl_output.py | 386 ++++++++++++++++++++++++++++++ uv.lock | 225 ++++++++++++++++- 9 files changed, 1076 insertions(+), 55 deletions(-) create mode 100644 scripts/bench_dsl_output.py create mode 100644 tests/test_dsl_output.py diff --git a/README.md b/README.md index 6396f788..295cd6a4 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,13 @@ Your AI assistant uses these automatically once the graph is built. | `list_repos_tool` | List registered repositories | | `cross_repo_search_tool` | Search across all registered repositories | +The three highest-payload tools — `get_impact_radius_tool`, +`query_graph_tool`, and `get_review_context_tool` — accept an optional +`format="dsl"` parameter that returns the graph payload as one-line +strings instead of JSON dicts (~2× smaller responses, ~8K tokens saved +per call on typical blast-radius queries). Default stays `format="dict"` +for backwards compatibility. + **MCP Prompts** (5 workflow templates): `review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check` diff --git a/code_review_graph/graph.py b/code_review_graph/graph.py index bde3e8ec..b930004f 100644 --- a/code_review_graph/graph.py +++ b/code_review_graph/graph.py @@ -1365,3 +1365,147 @@ def edge_to_dict(e: GraphEdge) -> dict: "file_path": e.file_path, "line": e.line, "confidence": e.confidence, "confidence_tier": e.confidence_tier, } + + +# --------------------------------------------------------------------------- +# DSL output mode — token-efficient one-line encoding for LLM consumption +# +# When a tool returns hundreds of nodes/edges, the JSON-dict form costs ~60 +# tokens per node and ~45 tokens per edge. The DSL form below is ~18 and ~12 +# respectively (3–4× compression), preserving every field that matters for +# downstream reasoning. Internal database IDs and the redundant +# confidence_tier (derivable from the float `confidence`) are dropped. +# --------------------------------------------------------------------------- + +_NODE_KIND_CODE: dict[str, str] = { + "Function": "fn", "Class": "cl", "File": "fi", + "Test": "tst", "Community": "com", +} + +_EDGE_KIND_CODE: dict[str, str] = { + "CALLS": "c", "IMPORTS_FROM": "i", "INHERITS": "h", + "CONTAINS": "n", "TESTED_BY": "t", "REFERENCES": "r", + "CROSS_COMMUNITY": "x", +} + +_LANG_CODE: dict[str, str] = { + "python": "py", "javascript": "js", "typescript": "ts", "tsx": "tsx", + "go": "go", "rust": "rs", "java": "jv", "csharp": "cs", "ruby": "rb", + "cpp": "cp", "c": "c", "kotlin": "kt", "swift": "sw", "php": "ph", + "scala": "sc", "solidity": "sl", "vue": "vu", "dart": "dr", "r": "r", + "perl": "pl", "lua": "lu", "objc": "m", "bash": "sh", + # Note: 'luau' intentionally not abbreviated to avoid collision with 'lua'. + # Languages absent from this table fall through verbose (e.g. 'powershell', + # 'elixir', 'svelte') — self-documenting names need no legend entry. +} + +DSL_LEGEND: str = ( + "# legend: nodes 'kind name@file:start-end lang [parent=...] [T=test]' " + "edges 'src→tgt kind @file:line conf' " + "kinds: fn=Function cl=Class fi=File tst=Test com=Community " + "edges: c=CALLS i=IMPORTS_FROM h=INHERITS n=CONTAINS t=TESTED_BY " + "r=REFERENCES x=CROSS_COMMUNITY " + "langs: py=python js=javascript ts=typescript tsx=tsx go=go rs=rust " + "jv=java cs=csharp rb=ruby cp=cpp c=c kt=kotlin sw=swift ph=php " + "sc=scala sl=solidity vu=vue dr=dart r=r pl=perl lu=lua m=objc sh=bash" +) + + +def _dsl_sanitize(s: str) -> str: + """Strip characters that would break the one-line-per-row DSL contract. + + Builds on ``_sanitize_name`` (which removes control chars but preserves + ``\\t`` and ``\\n`` because JSON can encode them) and additionally + collapses any remaining whitespace runs to a single space so a name like + ``"weird\\nname"`` doesn't split a DSL record across two lines. + """ + if s is None: + return "" + cleaned = _sanitize_name(s) + # Collapse tabs/newlines/multiple spaces to single space — DSL is line-oriented + return " ".join(cleaned.split()) + + +def node_to_dsl(n: GraphNode) -> str: + """Encode a node as a single compact DSL line. + + Format: `` @:- [parent=] [T]`` + + Drops the internal database ``id`` (not useful to LLMs) and folds the + ``qualified_name`` into the bare ``name`` + optional ``parent=`` suffix + (an LLM can reconstruct ``parent::name`` deterministically). The trailing + ``[T]`` flag is emitted only when ``is_test`` is true. + + Example: + ``fn validateOrder@src/orders/validator.py:142-178 py parent=OrderValidator`` + """ + kind = _NODE_KIND_CODE.get(n.kind, n.kind.lower()) + name = _dsl_sanitize(n.name) + # None language → empty string (rather than literal "None"). Known + # languages get their short code; unknown ones pass through verbose. + lang = _LANG_CODE.get(n.language, n.language) if n.language else "" + parts = [f"{kind} {name}@{n.file_path}:{n.line_start}-{n.line_end} {lang}".rstrip()] + if n.parent_name: + # Encode only the bare parent (last segment after `::`) to avoid + # repeating the file_path that already appears above. + parent = _dsl_sanitize(n.parent_name).rsplit("::", 1)[-1] + parts.append(f"parent={parent}") + if n.is_test: + parts.append("[T]") + return " ".join(parts) + + +def edge_to_dsl(e: GraphEdge) -> str: + """Encode an edge as a single compact DSL line. + + Format: `` @: `` + + Two redundancies are eliminated relative to the dict form: + + 1. The internal database ``id`` is dropped (not useful to LLMs). + 2. The ``confidence_tier`` string is dropped (fully derivable from the + float ``confidence``: ≥0.9 EXTRACTED, ≥0.5 INFERRED, else AMBIGUOUS). + 3. The ``file_path::`` prefix is stripped from ``source_qualified`` when + it matches the edge's ``file_path`` (which it does for edges + extracted from the source's containing file — the common case). + + Example:: + + OrderValidator::validateOrder→src/orders/db.py::Database::persist + c @src/orders/validator.py:160 0.95 + """ + kind = _EDGE_KIND_CODE.get(e.kind, e.kind.lower()) + src = _dsl_sanitize(e.source_qualified) + tgt = _dsl_sanitize(e.target_qualified) + # Strip redundant file_path prefix from source (it's already in @file:line) + if e.file_path and src.startswith(e.file_path + "::"): + src = src[len(e.file_path) + 2:] + return f"{src}\u2192{tgt} {kind} @{e.file_path}:{e.line} {e.confidence:.2f}" + + +def encode_nodes( + nodes: "list[GraphNode]", fmt: str = "dict", +) -> "list[dict] | list[str]": + """Encode a node list as dicts (default) or DSL strings. + + Args: + nodes: List of GraphNode instances. + fmt: ``"dict"`` (default, backwards compatible) or ``"dsl"``. + """ + if fmt == "dsl": + return [node_to_dsl(n) for n in nodes] + return [node_to_dict(n) for n in nodes] + + +def encode_edges( + edges: "list[GraphEdge]", fmt: str = "dict", +) -> "list[dict] | list[str]": + """Encode an edge list as dicts (default) or DSL strings. + + Args: + edges: List of GraphEdge instances. + fmt: ``"dict"`` (default, backwards compatible) or ``"dsl"``. + """ + if fmt == "dsl": + return [edge_to_dsl(e) for e in edges] + return [edge_to_dict(e) for e in edges] diff --git a/code_review_graph/main.py b/code_review_graph/main.py index 629b1674..9bbf5d61 100644 --- a/code_review_graph/main.py +++ b/code_review_graph/main.py @@ -193,6 +193,7 @@ def get_impact_radius_tool( repo_root: Optional[str] = None, base: str = "HEAD~1", detail_level: str = "standard", + format: str = "dict", ) -> dict: """Analyze the blast radius of changed files in the codebase. @@ -205,10 +206,12 @@ def get_impact_radius_tool( repo_root: Repository root path. Auto-detected if omitted. base: Git ref for auto-detecting changes. Default: HEAD~1. detail_level: "standard" for full output, "minimal" for compact summary. Default: standard. + format: "dict" (default) or "dsl" for compact line-based encoding (~3× fewer tokens). """ return get_impact_radius( changed_files=changed_files, max_depth=max_depth, repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level, + format=format, ) @@ -218,6 +221,7 @@ def query_graph_tool( target: str, repo_root: Optional[str] = None, detail_level: str = "standard", + format: str = "dict", ) -> dict: """Run a predefined graph query to explore code relationships. @@ -236,10 +240,11 @@ def query_graph_tool( target: Node name, qualified name, or file path to query. repo_root: Repository root path. Auto-detected if omitted. detail_level: "standard" for full output, "minimal" for compact summary. Default: standard. + format: "dict" (default) or "dsl" for compact line-based encoding (~3× fewer tokens). """ return query_graph( pattern=pattern, target=target, repo_root=_resolve_repo_root(repo_root), - detail_level=detail_level, + detail_level=detail_level, format=format, ) @@ -252,6 +257,7 @@ def get_review_context_tool( repo_root: Optional[str] = None, base: str = "HEAD~1", detail_level: str = "standard", + format: str = "dict", ) -> dict: """Generate a focused, token-efficient review context for code changes. @@ -267,11 +273,13 @@ def get_review_context_tool( base: Git ref for change detection. Default: HEAD~1. detail_level: "standard" for full output, "minimal" for token-efficient summary. Default: standard. + format: "dict" (default) or "dsl" for compact line-based encoding (~3× fewer tokens). """ return get_review_context( changed_files=changed_files, max_depth=max_depth, include_source=include_source, max_lines_per_file=max_lines_per_file, repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level, + format=format, ) diff --git a/code_review_graph/tools/query.py b/code_review_graph/tools/query.py index 3b442f8a..77aad37a 100644 --- a/code_review_graph/tools/query.py +++ b/code_review_graph/tools/query.py @@ -8,7 +8,16 @@ from ..context_savings import attach_context_savings, estimate_file_tokens from ..embeddings import EmbeddingStore -from ..graph import _sanitize_name, edge_to_dict, node_to_dict +from ..graph import ( + DSL_LEGEND, + _sanitize_name, + edge_to_dict, + edge_to_dsl, + encode_edges, + encode_nodes, + node_to_dict, + node_to_dsl, +) from ..hints import generate_hints, get_session from ..incremental import get_changed_files, get_db_path, get_staged_and_unstaged from ..search import hybrid_search @@ -39,6 +48,7 @@ def get_impact_radius( repo_root: str | None = None, base: str = "HEAD~1", detail_level: str = "standard", + format: str = "dict", ) -> dict[str, Any]: """Analyze the blast radius of changed files. @@ -50,6 +60,10 @@ def get_impact_radius( repo_root: Repository root path. Auto-detected if omitted. base: Git ref for auto-detecting changes (default: HEAD~1). detail_level: "standard" (full output) or "minimal" (summary only). + format: ``"dict"`` (default, returns lists of JSON-style objects) or + ``"dsl"`` (returns one-line DSL strings; ~3× fewer tokens). + When ``"dsl"``, the response also includes a ``legend`` field + with the decoding key. Returns: Changed nodes, impacted nodes, impacted files, connecting edges, @@ -80,34 +94,36 @@ def get_impact_radius( abs_files, max_depth=max_depth, max_nodes=max_results ) - changed_dicts = [node_to_dict(n) for n in result["changed_nodes"]] - impacted_dicts = [node_to_dict(n) for n in result["impacted_nodes"]] - edge_dicts = [edge_to_dict(e) for e in result["edges"]] + changed_encoded = encode_nodes(result["changed_nodes"], format) + impacted_encoded = encode_nodes(result["impacted_nodes"], format) + edge_encoded = encode_edges(result["edges"], format) truncated = result["truncated"] total_impacted = result["total_impacted"] summary_parts = [ f"Blast radius for {len(changed_files)} changed file(s):", - f" - {len(changed_dicts)} nodes directly changed", - f" - {len(impacted_dicts)} nodes impacted (within {max_depth} hops)", + f" - {len(changed_encoded)} nodes directly changed", + f" - {len(impacted_encoded)} nodes impacted (within {max_depth} hops)", f" - {len(result['impacted_files'])} additional files affected", ] if truncated: summary_parts.append( - f" - Results truncated: showing {len(impacted_dicts)}" + f" - Results truncated: showing {len(impacted_encoded)}" f" of {total_impacted} impacted nodes" ) if detail_level == "minimal": - impacted_count = len(impacted_dicts) + impacted_count = len(impacted_encoded) if impacted_count > 20: risk = "high" elif impacted_count > 5: risk = "medium" else: risk = "low" + # Key entities work for both formats — read .name on the original + # node objects rather than parsing the encoded form. key_entities = [ - n["name"] for n in impacted_dicts[:5] + n.name for n in result["impacted_nodes"][:5] ] minimal_response = { "status": "ok", @@ -124,13 +140,15 @@ def get_impact_radius( "status": "ok", "summary": "\n".join(summary_parts), "changed_files": changed_files, - "changed_nodes": changed_dicts, - "impacted_nodes": impacted_dicts, + "changed_nodes": changed_encoded, + "impacted_nodes": impacted_encoded, "impacted_files": result["impacted_files"], - "edges": edge_dicts, + "edges": edge_encoded, "truncated": truncated, "total_impacted": total_impacted, } + if format == "dsl": + response["legend"] = DSL_LEGEND attach_context_savings(response, original_tokens=original_tokens) return response finally: @@ -147,6 +165,7 @@ def query_graph( target: str, repo_root: str | None = None, detail_level: str = "standard", + format: str = "dict", ) -> dict[str, Any]: """Run a predefined graph query. @@ -156,10 +175,18 @@ def query_graph( target: The node name, qualified name, or file path to query about. repo_root: Repository root path. Auto-detected if omitted. detail_level: "standard" (full output) or "minimal" (summary only). + format: ``"dict"`` (default) or ``"dsl"`` for compact line-based output. + When ``"dsl"``, the response also includes a ``legend`` field. Returns: Matching nodes and edges for the query. """ + # Picker helpers: route node/edge encoding through the chosen format. + # Synthetic dicts (import targets, importer records) bypass this — they + # aren't real graph rows and stay as small dicts in both modes. + _n = node_to_dsl if format == "dsl" else node_to_dict + _e = edge_to_dsl if format == "dsl" else edge_to_dict + store, root = _get_store(repo_root) try: if pattern not in _QUERY_PATTERNS: @@ -171,8 +198,11 @@ def query_graph( ), } - results: list[dict] = [] - edges_out: list[dict] = [] + # Results may hold dicts (format='dict') or DSL strings (format='dsl'). + # Synthetic dicts for imports_of/importers_of stay as dicts in both + # modes (they aren't real graph rows). + results: list[Any] = [] + edges_out: list[Any] = [] # For callers_of, skip common builtins early (bare names only) # "Who calls .map()?" returns hundreds of useless hits. @@ -213,7 +243,7 @@ def query_graph( f"Multiple matches for '{target}'. " "Please use a qualified name." ), - "candidates": [node_to_dict(c) for c in candidates], + "candidates": [_n(c) for c in candidates], } if not node and pattern != "file_summary": @@ -232,8 +262,8 @@ def query_graph( seen_sources.add(e.source_qualified) caller = store.get_node(e.source_qualified) if caller: - results.append(node_to_dict(caller)) - edges_out.append(edge_to_dict(e)) + results.append(_n(caller)) + edges_out.append(_e(e)) # Fallback: CALLS edges store unqualified target names # (e.g. "generateTestCode") while qn is fully qualified # (e.g. "file.ts::generateTestCode"). Search by plain name too. @@ -243,8 +273,8 @@ def query_graph( seen_sources.add(e.source_qualified) caller = store.get_node(e.source_qualified) if caller: - results.append(node_to_dict(caller)) - edges_out.append(edge_to_dict(e)) + results.append(_n(caller)) + edges_out.append(_e(e)) elif pattern == "callees_of": seen_targets: set[str] = set() @@ -254,20 +284,20 @@ def query_graph( seen_targets.add(e.target_qualified) callee = store.get_node(e.target_qualified) if callee: - results.append(node_to_dict(callee)) + results.append(_n(callee)) elif "::" not in e.target_qualified: results.append({ "kind": "Function", "name": e.target_qualified, "qualified_name": e.target_qualified, }) - edges_out.append(edge_to_dict(e)) + edges_out.append(_e(e)) elif pattern == "imports_of": for e in store.get_edges_by_source(qn): if e.kind == "IMPORTS_FROM": results.append({"import_target": e.target_qualified}) - edges_out.append(edge_to_dict(e)) + edges_out.append(_e(e)) elif pattern == "importers_of": # Find edges where target matches this file. @@ -283,37 +313,39 @@ def query_graph( "importer": e.source_qualified, "file": e.file_path, }) - edges_out.append(edge_to_dict(e)) + edges_out.append(_e(e)) elif pattern == "children_of": for e in store.get_edges_by_source(qn): if e.kind == "CONTAINS": child = store.get_node(e.target_qualified) if child: - results.append(node_to_dict(child)) + results.append(_n(child)) elif pattern == "tests_for": + seen_qns: set[str] = set() for e in store.get_edges_by_target(qn): if e.kind == "TESTED_BY": test = store.get_node(e.source_qualified) - if test: - results.append(node_to_dict(test)) + if test and test.qualified_name not in seen_qns: + seen_qns.add(test.qualified_name) + results.append(_n(test)) # Also search by naming convention name = node.name if node else target test_nodes = store.search_nodes(f"test_{name}", limit=10) test_nodes += store.search_nodes(f"Test{name}", limit=10) - seen = {r.get("qualified_name") for r in results} for t in test_nodes: - if t.qualified_name not in seen and t.is_test: - results.append(node_to_dict(t)) + if t.qualified_name not in seen_qns and t.is_test: + seen_qns.add(t.qualified_name) + results.append(_n(t)) elif pattern == "inheritors_of": for e in store.get_edges_by_target(qn): if e.kind in ("INHERITS", "IMPLEMENTS"): child = store.get_node(e.source_qualified) if child: - results.append(node_to_dict(child)) - edges_out.append(edge_to_dict(e)) + results.append(_n(child)) + edges_out.append(_e(e)) # Fallback: INHERITS/IMPLEMENTS edges store unqualified base names # (e.g. "Animal") while qn is fully qualified # (e.g. "sample.dart::Animal"). Search by plain name too. See: #87 @@ -322,14 +354,14 @@ def query_graph( for e in store.search_edges_by_target_name(node.name, kind=kind): child = store.get_node(e.source_qualified) if child: - results.append(node_to_dict(child)) - edges_out.append(edge_to_dict(e)) + results.append(_n(child)) + edges_out.append(_e(e)) elif pattern == "file_summary": graph_paths = _resolve_graph_file_paths(store, root, [target]) for graph_path in graph_paths: for n in store.get_nodes_by_file(graph_path): - results.append(node_to_dict(n)) + results.append(_n(n)) summary = ( f"Found {len(results)} result(s) " @@ -337,14 +369,18 @@ def query_graph( ) if detail_level == "minimal": - minimal_results = [ - { - k: r[k] - for k in ("name", "kind", "file_path") - if k in r - } - for r in results[:5] - ] + if format == "dsl": + # DSL strings already contain everything; just truncate. + minimal_results: list = results[:5] + else: + minimal_results = [ + { + k: r[k] + for k in ("name", "kind", "file_path") + if isinstance(r, dict) and k in r + } + for r in results[:5] + ] return { "status": "ok", "pattern": pattern, @@ -355,7 +391,7 @@ def query_graph( "results": minimal_results, } - return { + response: dict[str, Any] = { "status": "ok", "pattern": pattern, "target": target, @@ -364,6 +400,9 @@ def query_graph( "results": results, "edges": edges_out, } + if format == "dsl": + response["legend"] = DSL_LEGEND + return response finally: store.close() diff --git a/code_review_graph/tools/review.py b/code_review_graph/tools/review.py index a896983e..ce284db0 100644 --- a/code_review_graph/tools/review.py +++ b/code_review_graph/tools/review.py @@ -9,7 +9,7 @@ from ..changes import analyze_changes, parse_diff_ranges, parse_git_diff_ranges # noqa: F401 from ..context_savings import attach_context_savings, estimate_file_tokens from ..flows import get_affected_flows as _get_affected_flows -from ..graph import edge_to_dict, node_to_dict +from ..graph import DSL_LEGEND, edge_to_dict, edge_to_dsl, node_to_dict, node_to_dsl from ..hints import generate_hints, get_session from ..incremental import get_changed_files, get_staged_and_unstaged from ._common import _get_store, _resolve_graph_file_paths @@ -30,6 +30,7 @@ def get_review_context( repo_root: str | None = None, base: str = "HEAD~1", detail_level: str = "standard", + format: str = "dict", ) -> dict[str, Any]: """Generate a focused review context from changed files. @@ -46,11 +47,16 @@ def get_review_context( "minimal" returns summary, risk level, changed/impacted file counts, top 5 key entity names, test gap count, and next tool suggestions. Default: "standard". + format: ``"dict"`` (default) or ``"dsl"`` for line-based subgraph + encoding (~3× fewer tokens). When ``"dsl"``, ``context.legend`` + is included with the decoder key. Returns: Structured review context with subgraph, source snippets, and review guidance. """ + _n = node_to_dsl if format == "dsl" else node_to_dict + _e = edge_to_dsl if format == "dsl" else edge_to_dict store, root = _get_store(repo_root) try: # Get impact radius first @@ -127,14 +133,16 @@ def get_review_context( "impacted_files": impact["impacted_files"], "graph": { "changed_nodes": [ - node_to_dict(n) for n in impact["changed_nodes"] + _n(n) for n in impact["changed_nodes"] ], "impacted_nodes": [ - node_to_dict(n) for n in impact["impacted_nodes"] + _n(n) for n in impact["impacted_nodes"] ], - "edges": [edge_to_dict(e) for e in impact["edges"]], + "edges": [_e(e) for e in impact["edges"]], }, } + if format == "dsl": + context["legend"] = DSL_LEGEND # Add source snippets for changed files if include_source: diff --git a/pyproject.toml b/pyproject.toml index 60df6120..dc110494 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,7 @@ norecursedirs = ["tests/fixtures"] [dependency-groups] dev = [ + "mypy>=2.1.0", "pytest>=8.4.2", "pytest-asyncio>=0.23,<1", ] diff --git a/scripts/bench_dsl_output.py b/scripts/bench_dsl_output.py new file mode 100644 index 00000000..cefc7c8d --- /dev/null +++ b/scripts/bench_dsl_output.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""DSL vs dict output-mode benchmark. + +Builds a realistic-shaped graph (functions across deep namespaces with +realistic file paths) and measures token reduction when returning blast-radius +and query_graph responses in dict vs dsl format. + +Usage: + python3 scripts/bench_dsl_output.py + +Token counts use a coarse 4-chars-per-token approximation (matching the +estimate used in code_review_graph/context_savings.py); for tiktoken-accurate +numbers, replace ``approx_tokens`` with a real tokenizer call. +""" +from __future__ import annotations + +import json +import sys +import tempfile +from pathlib import Path + +# Ensure package on path when running from a checkout +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from code_review_graph.graph import ( # noqa: E402 + EdgeInfo, + GraphStore, + NodeInfo, +) +from code_review_graph.tools.query import ( # noqa: E402 + get_impact_radius, + query_graph, +) + + +def approx_tokens(text: str) -> int: + """Rough ~4-chars-per-token estimate. Good enough for relative comparisons.""" + return max(1, len(text) // 4) + + +def seed_realistic_graph(store: GraphStore, root: Path, n_files: int = 50) -> str: + """Build a graph approximating a real backend service: + + - ``n_files`` Python files, each with one class and 5 functions + - Cross-file CALLS edges so a single changed file has a real blast radius + - Deep file paths so qualified_names are long (~70-100 chars), matching + real codebases where DSL compression matters most + """ + files: list[str] = [] + for i in range(n_files): + path = str( + root + / "src" + / "backend" + / "services" + / f"module_{i:02d}" + / "handlers.py" + ) + files.append(path) + store.upsert_node(NodeInfo( + kind="File", name=path, file_path=path, + line_start=1, line_end=500, language="python", + )) + store.upsert_node(NodeInfo( + kind="Class", name=f"Module{i:02d}Handler", file_path=path, + line_start=10, line_end=400, language="python", + )) + for j in range(5): + store.upsert_node(NodeInfo( + kind="Function", name=f"handle_request_{j}", file_path=path, + line_start=50 + j * 60, line_end=50 + j * 60 + 40, + language="python", + parent_name=f"Module{i:02d}Handler", + )) + + # Add cross-file CALLS so blast radius is rich: + # every module's handle_request_0 calls 4 other modules' handlers, + # producing fan-out that compounds at multi-hop depth. + for i in range(n_files): + src_path = files[i] + for k, offset in enumerate([1, 3, 7, 13]): + tgt_idx = (i + offset) % n_files + tgt_path = files[tgt_idx] + store.upsert_edge(EdgeInfo( + kind="CALLS", + source=f"{src_path}::Module{i:02d}Handler.handle_request_0", + target=f"{tgt_path}::Module{tgt_idx:02d}Handler.handle_request_{k}", + file_path=src_path, + line=55 + k, + )) + # And a CONTAINS edge for the class + store.upsert_edge(EdgeInfo( + kind="CONTAINS", + source=src_path, + target=f"{src_path}::Module{i:02d}Handler", + file_path=src_path, + line=10, + )) + + store.commit() + return files[0] # canonical "changed" file + + +def bench_impact_radius(changed_file: str, repo_root: str) -> None: + print("\n=== get_impact_radius ===") + dict_resp = get_impact_radius( + changed_files=[changed_file], + repo_root=repo_root, + max_depth=4, + format="dict", + ) + dsl_resp = get_impact_radius( + changed_files=[changed_file], + repo_root=repo_root, + max_depth=4, + format="dsl", + ) + dict_json = json.dumps(dict_resp) + dsl_json = json.dumps(dsl_resp, ensure_ascii=False) + n_nodes = len(dict_resp.get("impacted_nodes", [])) + n_edges = len(dict_resp.get("edges", [])) + print(f" impacted nodes: {n_nodes}") + print(f" edges: {n_edges}") + print(f" dict response: {len(dict_json):>7,} chars ~{approx_tokens(dict_json):>6,} tokens") + print(f" dsl response: {len(dsl_json):>7,} chars ~{approx_tokens(dsl_json):>6,} tokens") + if dsl_json: + ratio = len(dict_json) / max(1, len(dsl_json)) + print(f" compression: {ratio:.2f}× smaller " + f"({100 * (1 - len(dsl_json)/len(dict_json)):.1f}% fewer chars)") + print( + f" absolute save: " + f"~{approx_tokens(dict_json) - approx_tokens(dsl_json):,}" + f" tokens per call" + ) + + # Per-payload breakdown: just the nodes+edges arrays, no envelope + print("\n per-payload breakdown (nodes + edges only):") + dict_payload = json.dumps({ + "changed_nodes": dict_resp.get("changed_nodes", []), + "impacted_nodes": dict_resp.get("impacted_nodes", []), + "edges": dict_resp.get("edges", []), + }) + dsl_payload = json.dumps({ + "changed_nodes": dsl_resp.get("changed_nodes", []), + "impacted_nodes": dsl_resp.get("impacted_nodes", []), + "edges": dsl_resp.get("edges", []), + }, ensure_ascii=False) + if dsl_payload: + payload_ratio = len(dict_payload) / max(1, len(dsl_payload)) + print( + f" dict payload: {len(dict_payload):>7,} chars" + f" ~{approx_tokens(dict_payload):>6,} tokens" + ) + print( + f" dsl payload: {len(dsl_payload):>7,} chars" + f" ~{approx_tokens(dsl_payload):>6,} tokens" + ) + print(f" compression: {payload_ratio:.2f}× smaller " + f"({100 * (1 - len(dsl_payload)/len(dict_payload)):.1f}% fewer chars)") + + +def bench_query_graph(target: str, repo_root: str) -> None: + print("\n=== query_graph (callers_of, deep transitive) ===") + # callers_of returns dicts mixed with synthetic dicts; most realistic + # is file_summary against a large file + dict_resp = query_graph( + pattern="file_summary", + target=target, + repo_root=repo_root, + format="dict", + ) + dsl_resp = query_graph( + pattern="file_summary", + target=target, + repo_root=repo_root, + format="dsl", + ) + dict_json = json.dumps(dict_resp) + dsl_json = json.dumps(dsl_resp, ensure_ascii=False) + n_results = len(dict_resp.get("results", [])) + print(f" results: {n_results}") + print(f" dict response: {len(dict_json):>7,} chars ~{approx_tokens(dict_json):>6,} tokens") + print(f" dsl response: {len(dsl_json):>7,} chars ~{approx_tokens(dsl_json):>6,} tokens") + if dsl_json: + ratio = len(dict_json) / max(1, len(dsl_json)) + print(f" compression: {ratio:.2f}× smaller " + f"({100 * (1 - len(dsl_json)/len(dict_json)):.1f}% fewer chars)") + + +def main() -> int: + tmp_dir = tempfile.mkdtemp() + root = Path(tmp_dir).resolve() + (root / ".git").mkdir() + crg_dir = root / ".code-review-graph" + crg_dir.mkdir() + db_path = crg_dir / "graph.db" + + print(f"Seeding realistic graph at {root}...") + store = GraphStore(db_path) + try: + changed_file = seed_realistic_graph(store, root, n_files=50) + stats = store.get_stats() + print(f"Graph: {stats.total_nodes} nodes, {stats.total_edges} edges, " + f"{stats.files_count} files") + finally: + store.close() + + bench_impact_radius(changed_file, str(root)) + bench_query_graph(changed_file, str(root)) + + print("\nDone.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_dsl_output.py b/tests/test_dsl_output.py new file mode 100644 index 00000000..396e77e1 --- /dev/null +++ b/tests/test_dsl_output.py @@ -0,0 +1,386 @@ +"""Tests for the DSL output mode added to graph and tool layers. + +Covers: + * Unit tests for ``node_to_dsl`` / ``edge_to_dsl`` covering happy path, + optional fields (parent_name, is_test), unknown kinds, file_path prefix + stripping, and security parity with the dict encoder (control-char + sanitisation). + * ``encode_nodes`` / ``encode_edges`` dispatch on both ``"dict"`` and + ``"dsl"`` modes. + * Integration tests for ``get_impact_radius`` and ``query_graph`` with + ``format="dsl"`` — verifying the response shape, presence of the + legend, and that DSL output is meaningfully smaller than dict output. +""" +from __future__ import annotations + +import json +import tempfile +from pathlib import Path + +from code_review_graph.graph import ( + DSL_LEGEND, + EdgeInfo, + GraphEdge, + GraphNode, + GraphStore, + NodeInfo, + edge_to_dict, + edge_to_dsl, + encode_edges, + encode_nodes, + node_to_dict, + node_to_dsl, +) +from code_review_graph.tools.query import get_impact_radius, query_graph + +# --------------------------------------------------------------------------- +# Encoder unit tests +# --------------------------------------------------------------------------- + +def _mk_node(**overrides) -> GraphNode: + base = dict( + id=1, kind="Function", name="login", + qualified_name="/repo/auth.py::AuthService::login", + file_path="/repo/auth.py", line_start=10, line_end=20, + language="python", + parent_name="/repo/auth.py::AuthService", + params="self, user, pw", return_type="bool", + is_test=False, file_hash=None, extra={}, + ) + base.update(overrides) + return GraphNode(**base) + + +def _mk_edge(**overrides) -> GraphEdge: + base = dict( + id=1, kind="CALLS", + source_qualified="/repo/main.py::process", + target_qualified="/repo/auth.py::AuthService::login", + file_path="/repo/main.py", line=42, extra={}, + confidence=0.95, confidence_tier="EXTRACTED", + ) + base.update(overrides) + return GraphEdge(**base) + + +class TestNodeToDsl: + def test_function_with_parent(self): + out = node_to_dsl(_mk_node()) + assert out == "fn login@/repo/auth.py:10-20 py parent=AuthService" + + def test_function_top_level(self): + out = node_to_dsl(_mk_node(parent_name=None)) + # No parent= clause when there is no parent + assert out == "fn login@/repo/auth.py:10-20 py" + assert "parent=" not in out + + def test_class(self): + out = node_to_dsl(_mk_node( + kind="Class", name="AuthService", + qualified_name="/repo/auth.py::AuthService", + line_start=5, line_end=40, parent_name=None, + )) + assert out == "cl AuthService@/repo/auth.py:5-40 py" + + def test_file(self): + out = node_to_dsl(_mk_node( + kind="File", name="/repo/auth.py", + qualified_name="/repo/auth.py", + file_path="/repo/auth.py", line_start=1, line_end=50, + parent_name=None, + )) + assert out.startswith("fi ") + assert "/repo/auth.py" in out + + def test_is_test_flag(self): + out = node_to_dsl(_mk_node( + kind="Test", name="test_login", + file_path="/repo/test_auth.py", + line_start=1, line_end=10, is_test=True, parent_name=None, + )) + assert "[T]" in out + assert out.startswith("tst ") + + def test_unknown_language_falls_through(self): + out = node_to_dsl(_mk_node(language="brainfuck")) + # Unknown languages are emitted as-is rather than dropped + assert " brainfuck " in out + " " + + def test_lua_and_luau_distinguishable(self): + """lua and luau must produce different DSL tokens — no collision.""" + lua_out = node_to_dsl(_mk_node(language="lua", parent_name=None)) + luau_out = node_to_dsl(_mk_node(language="luau", parent_name=None)) + assert lua_out != luau_out + assert " lu" in lua_out # short code for lua + assert " luau" in luau_out # falls through to verbose + + def test_unknown_kind_falls_through(self): + # Custom kinds (e.g. a future "Macro" type) should not crash; + # they degrade to their lowercased form. + out = node_to_dsl(_mk_node(kind="Macro", parent_name=None)) + assert out.startswith("macro ") + + def test_control_chars_sanitised(self): + """Security parity with node_to_dict — control chars stripped.""" + n = _mk_node( + name="evil\x00name", + qualified_name="/repo/auth.py::evil\x00name", + parent_name=None, + ) + out = node_to_dsl(n) + assert "\x00" not in out + + def test_newlines_in_name_collapsed(self): + """Newlines/tabs must not break the one-line-per-row DSL contract.""" + n = _mk_node(name="weird\nname\twith\nbreaks", parent_name=None) + out = node_to_dsl(n) + assert "\n" not in out + assert "\t" not in out + # Should still preserve the readable content as one line + assert "weird" in out and "name" in out + + def test_none_language_handled(self): + """A node with language=None must not emit literal 'None'.""" + n = _mk_node(language=None, parent_name=None) + out = node_to_dsl(n) + assert "None" not in out + assert " none " not in out.lower() + + +class TestEdgeToDsl: + def test_basic_call(self): + out = edge_to_dsl(_mk_edge()) + # source file_path (/repo/main.py) IS the edge file_path, + # so the prefix gets stripped + assert out == "process\u2192/repo/auth.py::AuthService::login c @/repo/main.py:42 0.95" + + def test_source_prefix_stripped(self): + """Edge source's file_path prefix gets stripped when it equals the + edge's file_path — that's the common case for intra-file relations.""" + out = edge_to_dsl(_mk_edge( + source_qualified="/repo/a.py::foo", + file_path="/repo/a.py", + )) + assert out.startswith("foo\u2192") + assert "/repo/a.py::foo" not in out # full source qn stripped + + def test_source_prefix_not_stripped_when_mismatch(self): + """If source's file_path differs from edge's file_path (rare but + possible for cross-file relations), no stripping happens.""" + out = edge_to_dsl(_mk_edge( + source_qualified="/repo/other.py::foo", + file_path="/repo/a.py", + )) + assert "/repo/other.py::foo" in out + + def test_edge_kind_codes(self): + for kind, code in [ + ("CALLS", "c"), ("IMPORTS_FROM", "i"), + ("INHERITS", "h"), ("CONTAINS", "n"), + ("TESTED_BY", "t"), ("REFERENCES", "r"), + ]: + out = edge_to_dsl(_mk_edge(kind=kind)) + assert f" {code} @" in out, f"{kind} did not produce ' {code} @'" + + def test_unknown_edge_kind_falls_through(self): + out = edge_to_dsl(_mk_edge(kind="FOLLOWS")) + assert " follows @" in out + + def test_control_chars_sanitised(self): + e = _mk_edge( + source_qualified="/repo/main.py::ev\x00il", + target_qualified="/repo/auth.py::lo\x01gin", + ) + out = edge_to_dsl(e) + assert "\x00" not in out and "\x01" not in out + + +# --------------------------------------------------------------------------- +# Dispatch helpers +# --------------------------------------------------------------------------- + +class TestEncodeHelpers: + def test_encode_nodes_dict(self): + out = encode_nodes([_mk_node()], fmt="dict") + assert isinstance(out[0], dict) + assert out[0]["name"] == "login" + + def test_encode_nodes_dsl(self): + out = encode_nodes([_mk_node()], fmt="dsl") + assert isinstance(out[0], str) + assert out[0].startswith("fn login@") + + def test_encode_edges_dict(self): + out = encode_edges([_mk_edge()], fmt="dict") + assert isinstance(out[0], dict) + assert out[0]["kind"] == "CALLS" + + def test_encode_edges_dsl(self): + out = encode_edges([_mk_edge()], fmt="dsl") + assert isinstance(out[0], str) + assert "\u2192" in out[0] # arrow present + + def test_encode_nodes_default_is_dict(self): + out = encode_nodes([_mk_node()]) + assert isinstance(out[0], dict) + + +# --------------------------------------------------------------------------- +# Compression sanity check +# --------------------------------------------------------------------------- + +class TestCompression: + def test_node_dsl_significantly_smaller(self): + n = _mk_node( + qualified_name="/repo/services/orders/validator.py" + "::OrderValidator::validateOrder", + file_path="/repo/services/orders/validator.py", + parent_name="/repo/services/orders/validator.py::OrderValidator", + line_start=142, line_end=178, + ) + dict_len = len(json.dumps(node_to_dict(n))) + dsl_len = len(node_to_dsl(n)) + # Realistic nodes compress at least 3× + assert dict_len / dsl_len >= 3.0, ( + f"expected ≥3× compression, got {dict_len/dsl_len:.2f}× " + f"({dict_len} → {dsl_len})" + ) + + def test_edge_dsl_smaller(self): + e = _mk_edge( + source_qualified="/repo/services/orders/validator.py" + "::OrderValidator::validateOrder", + target_qualified="/repo/services/orders/db.py::Database::persist", + file_path="/repo/services/orders/validator.py", + ) + dict_len = len(json.dumps(edge_to_dict(e))) + dsl_len = len(edge_to_dsl(e)) + assert dict_len / dsl_len >= 2.0, ( + f"expected ≥2× compression, got {dict_len/dsl_len:.2f}× " + f"({dict_len} → {dsl_len})" + ) + + +# --------------------------------------------------------------------------- +# Tool integration tests (use a real GraphStore on disk) +# --------------------------------------------------------------------------- + +class TestToolIntegration: + def setup_method(self): + # Build a fake repo dir so _validate_repo_root accepts it + self.tmp_dir = tempfile.mkdtemp() + self.root = Path(self.tmp_dir).resolve() + (self.root / ".git").mkdir() + crg_dir = self.root / ".code-review-graph" + crg_dir.mkdir() + self.db_path = crg_dir / "graph.db" + self.store = GraphStore(self.db_path) + self._seed() + + def teardown_method(self): + self.store.close() + import shutil + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + def _seed(self): + s = self.store + auth_path = str(self.root / "auth.py") + main_path = str(self.root / "main.py") + + s.upsert_node(NodeInfo( + kind="File", name=auth_path, file_path=auth_path, + line_start=1, line_end=50, language="python", + )) + s.upsert_node(NodeInfo( + kind="File", name=main_path, file_path=main_path, + line_start=1, line_end=30, language="python", + )) + s.upsert_node(NodeInfo( + kind="Class", name="AuthService", file_path=auth_path, + line_start=5, line_end=40, language="python", + )) + s.upsert_node(NodeInfo( + kind="Function", name="login", file_path=auth_path, + line_start=10, line_end=20, language="python", + parent_name="AuthService", + )) + s.upsert_node(NodeInfo( + kind="Function", name="process", file_path=main_path, + line_start=5, line_end=15, language="python", + )) + s.upsert_edge(EdgeInfo( + kind="CALLS", source=f"{main_path}::process", + target=f"{auth_path}::AuthService.login", + file_path=main_path, line=10, + )) + s.commit() + self.auth_path = auth_path + self.main_path = main_path + + def test_get_impact_radius_dsl_mode(self): + result = get_impact_radius( + changed_files=[self.main_path], + repo_root=str(self.root), + format="dsl", + ) + assert result["status"] == "ok" + assert "legend" in result + assert result["legend"] == DSL_LEGEND + # changed_nodes should now be DSL strings, not dicts + for line in result["changed_nodes"]: + assert isinstance(line, str) + # Each DSL line carries '@' between name and file_path + assert "@" in line + + def test_get_impact_radius_dict_mode_unchanged(self): + """Default mode must remain identical to pre-PR behaviour.""" + result = get_impact_radius( + changed_files=[self.main_path], + repo_root=str(self.root), + ) + assert result["status"] == "ok" + assert "legend" not in result + for n in result["changed_nodes"]: + assert isinstance(n, dict) + assert "name" in n and "kind" in n + + def test_get_impact_radius_dsl_smaller_than_dict(self): + dict_resp = get_impact_radius( + changed_files=[self.main_path], + repo_root=str(self.root), + format="dict", + ) + dsl_resp = get_impact_radius( + changed_files=[self.main_path], + repo_root=str(self.root), + format="dsl", + ) + dict_size = len(json.dumps(dict_resp)) + dsl_size = len(json.dumps(dsl_resp)) + # Even on a tiny 5-node graph, DSL should not be larger + assert dsl_size <= dict_size, ( + f"DSL response ({dsl_size} chars) larger than dict ({dict_size})" + ) + + def test_query_graph_dsl_mode(self): + result = query_graph( + pattern="callees_of", + target=f"{self.main_path}::process", + repo_root=str(self.root), + format="dsl", + ) + assert result["status"] == "ok" + assert result["legend"] == DSL_LEGEND + # results may be DSL strings (real nodes) or small synthetic dicts + for r in result["results"]: + assert isinstance(r, (str, dict)) + + def test_query_graph_dict_mode_default(self): + result = query_graph( + pattern="callees_of", + target=f"{self.main_path}::process", + repo_root=str(self.root), + ) + assert "legend" not in result + for r in result["results"]: + assert isinstance(r, dict) diff --git a/uv.lock b/uv.lock index a3b85816..038ba61b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -44,6 +45,46 @@ 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 = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -386,6 +427,7 @@ wiki = [ [package.dev-dependencies] dev = [ + { name = "mypy" }, { name = "pytest" }, { name = "pytest-asyncio" }, ] @@ -422,6 +464,7 @@ provides-extras = ["embeddings", "google-embeddings", "communities", "eval", "wi [package.metadata.requires-dev] dev = [ + { name = "mypy", specifier = ">=2.1.0" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-asyncio", specifier = ">=0.23,<1" }, ] @@ -510,7 +553,8 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -1003,7 +1047,8 @@ name = "google-api-core" version = "2.25.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", ] dependencies = [ { name = "google-auth", marker = "python_full_version >= '3.14'" }, @@ -1643,6 +1688,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1867,6 +1997,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "networkx" version = "3.4.2" @@ -1884,7 +2082,8 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -1964,7 +2163,8 @@ name = "numpy" version = "2.4.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -2243,6 +2443,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pillow" version = "12.1.1" @@ -3206,7 +3415,8 @@ name = "scikit-learn" version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -3321,7 +3531,8 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'",