diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7a11b0a4f..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08d30b3e2..548b40f22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,9 +79,9 @@ jobs: uv run --frozen graphify install security-scan: - # The dev deps already include bandit, pip-audit, and safety. Run them in - # CI so a new HIGH-severity finding or vulnerable dependency is caught on - # the PR that introduces it, rather than at the next manual audit. + # The dev deps include bandit and pip-audit. Run them in CI so a new + # HIGH-severity finding or vulnerable dependency is caught on the PR that + # introduces it, rather than at the next manual audit. # Non-blocking for now (continue-on-error) to avoid breaking CI on # pre-existing findings; remove continue-on-error after the initial # cleanup pass. diff --git a/docs/.DS_Store b/docs/.DS_Store deleted file mode 100644 index b7a8a999f..000000000 Binary files a/docs/.DS_Store and /dev/null differ diff --git a/docs/translations/README.ja-JP.md b/docs/translations/README.ja-JP.md index f83c99c3f..467ff6839 100644 --- a/docs/translations/README.ja-JP.md +++ b/docs/translations/README.ja-JP.md @@ -114,7 +114,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski ``` - **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify` -When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. ``` diff --git a/docs/translations/README.ko-KR.md b/docs/translations/README.ko-KR.md index aee7776fb..0fa46a211 100644 --- a/docs/translations/README.ko-KR.md +++ b/docs/translations/README.ko-KR.md @@ -150,7 +150,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski ``` - **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify` -When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. ``` diff --git a/docs/translations/README.zh-CN.md b/docs/translations/README.zh-CN.md index 0aa194cc3..e41832293 100644 --- a/docs/translations/README.zh-CN.md +++ b/docs/translations/README.zh-CN.md @@ -110,7 +110,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski ``` - **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify` -When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. ``` diff --git a/graphify/.DS_Store b/graphify/.DS_Store deleted file mode 100644 index 0020f419f..000000000 Binary files a/graphify/.DS_Store and /dev/null differ diff --git a/graphify/__main__.py b/graphify/__main__.py index 2bad90a15..312f3eefe 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -462,8 +462,8 @@ def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md") "\n# graphify\n" f"- **graphify** (`{skill_path}`) " "- any input to knowledge graph. Trigger: `/graphify`\n" - "When the user types `/graphify`, invoke the Skill tool " - "with `skill: \"graphify\"` before doing anything else.\n" + "When the user types `/graphify`, use the installed graphify skill " + "or instructions before doing anything else.\n" ) @@ -2970,13 +2970,18 @@ def main() -> None: p = _ap.ArgumentParser(prog="graphify save-result") p.add_argument("--question", required=True) - p.add_argument("--answer", required=True) + p.add_argument("--answer", default=None) + p.add_argument("--answer-file", dest="answer_file", default=None) p.add_argument("--type", dest="query_type", default="query") p.add_argument("--nodes", nargs="*", default=[]) p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None) p.add_argument("--correction", default=None) p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) opts = p.parse_args(sys.argv[2:]) + if opts.answer_file: + opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip() + elif not opts.answer: + p.error("--answer or --answer-file is required") from graphify.ingest import save_query_result as _sqr out = _sqr( @@ -4546,6 +4551,7 @@ def _parse_float(name: str, raw: str) -> float: # Semantic extraction on docs/papers/images. Check cache first. from graphify.cache import ( check_semantic_cache as _check_semantic_cache, + prune_semantic_cache as _prune_semantic_cache, save_semantic_cache as _save_semantic_cache, ) sem_result: dict = { @@ -4636,6 +4642,32 @@ def _progress(idx: int, total: int, _result: dict) -> None: sem_result["hyperedges"].extend(fresh.get("hyperedges", [])) sem_result["input_tokens"] += fresh.get("input_tokens", 0) sem_result["output_tokens"] += fresh.get("output_tokens", 0) + + # Prune orphaned semantic cache entries. The semantic cache is + # content-hash-keyed and unversioned, so it is never swept by the AST + # version-cleanup: every content change or file deletion leaves a + # permanent orphan that accumulates unbounded (#1527). Sweep it against + # the FULL live document set (``files_by_type`` — present in both the + # incremental and full branches), NOT the incremental ``semantic_files`` + # changed-subset, which would delete every unchanged doc's valid entry. + # Best-effort: a prune failure must never break extraction. + try: + from graphify.cache import file_hash as _file_hash + _live_hashes: set[str] = set() + for _kind in ("document", "paper", "image"): + for _fp in files_by_type.get(_kind, []): + _abs = Path(_fp) + if not _abs.is_absolute(): + _abs = Path(out_root) / _abs + if not _abs.is_file(): + continue # deleted/missing — leave out so its entry is pruned + try: + _live_hashes.add(_file_hash(_abs, out_root)) + except OSError: + pass + _prune_semantic_cache(out_root, _live_hashes) + except Exception as exc: + print(f"[graphify extract] warning: could not prune semantic cache: {exc}", file=sys.stderr) stages.mark("semantic extract") pg_result: dict = {"nodes": [], "edges": []} diff --git a/graphify/always_on/agents-md.md b/graphify/always_on/agents-md.md index 20cff728b..6511cd1dd 100644 --- a/graphify/always_on/agents-md.md +++ b/graphify/always_on/agents-md.md @@ -2,7 +2,7 @@ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. -When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. diff --git a/graphify/cache.py b/graphify/cache.py index c00eaa557..f377889ff 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -407,6 +407,44 @@ def clear_cache(root: Path = Path(".")) -> None: f.unlink() +def prune_semantic_cache(root: Path, live_hashes: set[str]) -> int: + """Remove orphaned semantic cache entries, returning the count pruned. + + The semantic cache is content-hash-keyed (``{file_hash}.json`` under + ``cache/semantic/``) and deliberately UNVERSIONED — entries are produced by + the LLM from file contents, so invalidating them on every release would + re-bill extraction. Because it is unversioned it is also never swept by the + AST version-cleanup, so every content change or file deletion leaves a + permanent orphan entry that accumulates unbounded. + + This sweeps ``cache/semantic/*.json`` and deletes any entry whose stem (the + content hash) is not in ``live_hashes`` — the hashes of the current live + document set. ``*.tmp`` atomic-write temporaries are skipped, and only this + directory is touched (never ``cache/ast/**`` or anything else). The + unversioned design is preserved: we prune by liveness, not by version. + + Best-effort, mirroring :func:`_cleanup_stale_ast_entries`: each unlink is + wrapped in ``try/except OSError`` and a failure is ignored. The worst-case + failure mode is benign — a surviving orphan costs only one re-extraction of + one doc on a future run, never incorrect output. + """ + _out = Path(_GRAPHIFY_OUT) + base = _out if _out.is_absolute() else Path(root).resolve() / _out + semantic_dir = base / "cache" / "semantic" + if not semantic_dir.is_dir(): + return 0 + pruned = 0 + for entry in semantic_dir.glob("*.json"): + if entry.stem in live_hashes: + continue + try: + entry.unlink() + pruned += 1 + except OSError: + pass + return pruned + + def check_semantic_cache( files: list[str], root: Path = Path("."), diff --git a/graphify/export.py b/graphify/export.py index 052dbcc6a..29676036f 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -1535,6 +1535,15 @@ def to_graphml( for _, _, attrs in H.edges(data=True): for k in [k for k in attrs if k.startswith("_")]: del attrs[k] + # nx.write_graphml raises ValueError on None attribute values; replace with "". + for node_id in H.nodes(): + for key, val in list(H.nodes[node_id].items()): + if val is None: + H.nodes[node_id][key] = "" + for u, v in H.edges(): + for key, val in list(H.edges[u, v].items()): + if val is None: + H.edges[u, v][key] = "" nx.write_graphml(H, output_path) diff --git a/graphify/extract.py b/graphify/extract.py index 61f8afb5e..3ee27dd59 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -14,6 +14,12 @@ from .cache import load_cached, save_cached from .mcp_ingest import extract_mcp_config, is_mcp_config_path from .manifest_ingest import extract_package_manifest, is_package_manifest_path +from .resolver_registry import ( + LanguageResolver, + register as register_language_resolver, + run_language_resolvers, +) +from .ruby_resolution import resolve_ruby_member_calls # --- migrated to graphify/extractors/ (see graphify/extractors/MIGRATION.md) --- from graphify.extractors.base import ( # noqa: F401 @@ -70,7 +76,7 @@ def _file_node_id(rel_path: Path) -> str: return _make_id(_file_stem(rel_path)) -_TSCONFIG_ALIAS_CACHE: dict[str, dict[str, str]] = {} +_TSCONFIG_ALIAS_CACHE: dict[str, dict[str, list[str]]] = {} _WORKSPACE_PACKAGE_CACHE: dict[str, dict[str, Path]] = {} _WORKSPACE_MANIFEST_NAMES = ("pnpm-workspace.yaml", "package.json") _JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".ts", ".tsx", ".vue", ".svelte"} @@ -177,7 +183,7 @@ def _replace(match: re.Match) -> str: return stripped -def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[str, str]: +def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[str, list[str]]: """Recursively read path aliases from a tsconfig, following extends chains. Child config paths override parent. Circular extends are detected via seen set. @@ -205,7 +211,7 @@ def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[st print(f" warning: failed to parse {tsconfig} ({type(e).__name__}: {e})", file=sys.stderr, flush=True) return {} - aliases: dict[str, str] = {} + aliases: dict[str, list[str]] = {} # `extends` may be a string or, since TypeScript 5.0, an array of paths. # For an array, parents are processed in order with later entries # overriding earlier ones; the extending config (paths below) overrides @@ -244,17 +250,26 @@ def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[st if not targets: continue alias_prefix = alias.rstrip("/*") - target_base = targets[0].rstrip("/*") - aliases[alias_prefix] = str(os.path.normpath(paths_base / target_base)) + # Keep ALL targets in declared order — tsc tries each until one resolves + # on disk. Discarding the fallbacks (#1531) misresolved/dropped imports + # whose file lived at a non-first target. Empty/non-string entries skipped. + target_bases = [ + str(os.path.normpath(paths_base / t.rstrip("/*"))) + for t in targets + if isinstance(t, str) and t + ] + if target_bases: + aliases[alias_prefix] = target_bases return aliases -def _load_tsconfig_aliases(start_dir: Path) -> dict[str, str]: +def _load_tsconfig_aliases(start_dir: Path) -> dict[str, list[str]]: """Walk up from start_dir to find tsconfig.json and return compilerOptions.paths aliases. Follows extends chains so SvelteKit/Nuxt/NestJS inherited aliases are included. - Returns a dict mapping alias prefix (e.g. "@/") to resolved base dir (e.g. "src/"). + Returns a dict mapping alias prefix (e.g. "@") to an ordered list of resolved + base dirs (e.g. ["src"]) — tsc tries each in declared order (#1531). Result is cached by tsconfig path string. """ current = start_dir.resolve() @@ -268,6 +283,26 @@ def _load_tsconfig_aliases(start_dir: Path) -> dict[str, str]: return {} +def _resolve_tsconfig_alias(raw: str, aliases: dict[str, list[str]]) -> "Path | None": + """Resolve `raw` against tsconfig path aliases. Try each target in declared + order; return the first whose candidate resolves to a real file (tsc parity). + If none exist, return the first candidate (no false edge fabricated, prior + single-target behavior preserved). Returns a Path or None if no alias matches.""" + for alias_prefix, alias_bases in aliases.items(): + if raw == alias_prefix or raw.startswith(alias_prefix + "/"): + rest = raw[len(alias_prefix):].lstrip("/") + first = None + for base in alias_bases: + cand = Path(os.path.normpath(Path(base) / rest)) + resolved = _resolve_js_import_path(cand) + if resolved.is_file(): + return resolved + if first is None: + first = cand + return first + return None + + def _find_workspace_root(start_dir: Path) -> Path | None: current = start_dir.resolve() for candidate in [current, *current.parents]: @@ -356,6 +391,42 @@ def _load_workspace_packages(start_dir: Path) -> dict[str, Path]: return packages +# Condition keys consulted when resolving an `exports` target, in priority +# order. `default` is Node's catch-all and must be consulted LAST so a more +# specific condition (source/import/module/etc.) wins when several match. +_EXPORT_CONDITION_PRIORITY = ( + "source", "import", "module", "svelte", "types", "require", "default", +) + + +def _resolve_export_target(value: Any) -> str | None: + """Resolve an `exports` map value (string or condition object) to a + relative target string, honouring _EXPORT_CONDITION_PRIORITY for objects + and recursing into nested condition objects.""" + if isinstance(value, str): + return value + if isinstance(value, dict): + for cond in _EXPORT_CONDITION_PRIORITY: + v = value.get(cond) + if isinstance(v, str): + return v + if isinstance(v, dict): + nested = _resolve_export_target(v) + if nested: + return nested + return None + + +def _contained_in_package(resolved: Path, package_dir: Path) -> bool: + """Guard against `exports` targets that escape the package directory + (e.g. "./evil": "../../../etc/passwd"). Only accept paths that stay + within package_dir after resolution.""" + try: + return resolved.resolve().is_relative_to(package_dir.resolve()) + except ValueError: + return False + + def _package_entry_candidates(package_dir: Path, subpath: str) -> list[Path]: manifest = package_dir / "package.json" manifest_data: dict[str, Any] = {} @@ -365,20 +436,39 @@ def _package_entry_candidates(package_dir: Path, subpath: str) -> list[Path]: pass if subpath: + # Consult the package's `exports` subpath map before the bare-path + # fallback (#1308): "./browser" -> conditions -> file, plus single + # wildcard "./*" patterns. Targets that escape the package dir are + # rejected; resolution then falls through to the bare path. + exports = manifest_data.get("exports") + if isinstance(exports, dict): + subpath_key = "./" + subpath + target = _resolve_export_target(exports.get(subpath_key)) + if target: + candidate = package_dir / target + if _contained_in_package(candidate, package_dir): + return [candidate] + else: + for pattern, pattern_value in exports.items(): + if "*" in pattern and pattern.count("*") == 1: + prefix, suffix = pattern.split("*", 1) + if (subpath_key.startswith(prefix) + and (not suffix or subpath_key.endswith(suffix))): + matched = subpath_key[len(prefix):len(subpath_key) - len(suffix) if suffix else None] + resolved = _resolve_export_target(pattern_value) + if resolved and "*" in resolved: + candidate = package_dir / resolved.replace("*", matched) + if _contained_in_package(candidate, package_dir): + return [candidate] return [package_dir / subpath] exports = manifest_data.get("exports") if isinstance(exports, str): return [package_dir / exports] if isinstance(exports, dict): - dot_export = exports.get(".") - if isinstance(dot_export, str): - return [package_dir / dot_export] - if isinstance(dot_export, dict): - for key in ("types", "import", "default", "svelte"): - value = dot_export.get(key) - if isinstance(value, str): - return [package_dir / value] + dot_target = _resolve_export_target(exports.get(".")) + if dot_target: + return [package_dir / dot_target] candidates: list[Path] = [] for key in ("svelte", "module", "main", "types"): @@ -422,10 +512,9 @@ def _resolve_js_module_path(raw: str | Path, start_dir: Path | None = None) -> P return _resolve_js_import_path(start_dir / raw) aliases = _load_tsconfig_aliases(start_dir) - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - return _resolve_js_import_path(Path(os.path.normpath(Path(alias_base) / rest))) + hit = _resolve_tsconfig_alias(raw, aliases) + if hit is not None: + return _resolve_js_import_path(hit) return _resolve_workspace_import(raw, start_dir) @@ -2391,6 +2480,63 @@ def _read_csharp_type_name(node, source: bytes) -> str | None: import_handler=_import_swift, ) +# ── Ruby local type inference (for member-call resolution) ───────────────────── + + +def _ruby_new_class_name(node, source: bytes) -> str | None: + """Return ``ClassName`` if ``node`` is a ``ClassName.new(...)`` call, else None. + + Only a bare capitalized constant receiver counts (``Processor.new``); + namespaced (``A::B.new``) and dynamic receivers are intentionally ignored so + the binding stays unambiguous. + """ + if node is None or node.type != "call": + return None + recv = node.child_by_field_name("receiver") + meth = node.child_by_field_name("method") + if recv is None or meth is None: + return None + if recv.type != "constant" or _read_text(meth, source) != "new": + return None + return _read_text(recv, source) + + +def _ruby_local_class_bindings(body_node, source: bytes) -> dict[str, str | None]: + """Map ``local_var -> ClassName`` for ``var = ClassName.new`` within one Ruby + method body, not descending into nested method definitions. + + 100%-confidence contract: a variable assigned more than once, or to anything + other than a single ``Constant.new``, maps to ``None`` (ambiguous) so callers + never resolve it. Only the certain single-binding case carries a type. + """ + bindings: dict[str, str | None] = {} + boundary = {"method", "singleton_method"} + + def visit(n) -> None: + for child in n.children: + if child.type in boundary: + continue # nested method has its own scope + if child.type == "assignment": + left = child.child_by_field_name("left") + right = child.child_by_field_name("right") + if left is not None and left.type == "identifier": + var = _read_text(left, source) + cls = _ruby_new_class_name(right, source) if right is not None else None + if cls is None: + # assigned to something we can't type: poison if it was typed + if var in bindings: + bindings[var] = None + elif var in bindings: + if bindings[var] != cls: + bindings[var] = None # reassigned to a different class + else: + bindings[var] = cls + visit(child) + + visit(body_node) + return bindings + + # ── Generic extractor ───────────────────────────────────────────────────────── def _extract_generic( @@ -3567,6 +3713,10 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None: seen_helper_ref_pairs: set[tuple[str, str, str]] = set() seen_bind_pairs: set[tuple[str, str, str]] = set() raw_calls: list[dict] = [] # unresolved calls for cross-file resolution in extract() + # Ruby: per-method `var -> ClassName` table from `var = Const.new` bindings, + # populated before walk_calls runs. Lets member-call raw_calls carry a + # receiver_type so the cross-file pass resolves `var.method` by type (#ruby). + ruby_var_types: dict[str, dict[str, str | None]] = {} def _php_class_const_scope(n) -> str | None: scope = n.child_by_field_name("scope") @@ -3701,6 +3851,20 @@ def walk_calls(node, caller_nid: str) -> None: raw = _read_text(type_node, source).split("<", 1)[0].strip() if raw: callee_name = raw.rsplit(".", 1)[-1] + elif config.ts_module == "tree_sitter_ruby": + # Ruby's `call` node carries `receiver` and `method` as direct + # fields (no intermediate accessor node), so the generic accessor + # model doesn't apply. Read them directly and capture a simple + # receiver (`p` in `p.run`, `Processor` in `Processor.new`) so the + # cross-file pass can resolve member calls by the receiver's type. + meth = node.child_by_field_name("method") + if meth is not None: + callee_name = _read_text(meth, source) + recv = node.child_by_field_name("receiver") + if recv is not None: + is_member_call = True + if recv.type in ("identifier", "constant"): + member_receiver = _read_text(recv, source) else: # Generic: get callee from call_function_field func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None @@ -3753,14 +3917,21 @@ def walk_calls(node, caller_nid: str) -> None: }) elif callee_name and not tgt_nid: # Callee not in this file — save for cross-file resolution in extract() - raw_calls.append({ + rc_entry = { "caller_nid": caller_nid, "callee": callee_name, "is_member_call": is_member_call, "source_file": str_path, "source_location": f"L{node.start_point[0] + 1}", "receiver": swift_receiver or member_receiver, - }) + } + # Ruby: attach the receiver's inferred type from the method's + # local `var = Const.new` bindings, when unambiguously known. + if member_receiver and config.ts_module == "tree_sitter_ruby": + rc_entry["receiver_type"] = ruby_var_types.get( + caller_nid, {} + ).get(member_receiver) + raw_calls.append(rc_entry) # Helper function calls: config('foo.bar') → uses_config edge to "foo" if (callee_name and callee_name in config.helper_fn_names): @@ -3889,6 +4060,10 @@ def walk_calls(node, caller_nid: str) -> None: for child in node.children: walk_calls(child, caller_nid) + if config.ts_module == "tree_sitter_ruby": + for caller_nid, body_node in function_bodies: + ruby_var_types[caller_nid] = _ruby_local_class_bindings(body_node, source) + for caller_nid, body_node in function_bodies: walk_calls(body_node, caller_nid) @@ -4121,12 +4296,7 @@ def extract_svelte(path: Path) -> dict: # Check tsconfig.json path aliases (e.g. "$lib/" -> "src/lib/", "@/" -> "src/") # before treating as external. Mirrors _import_js logic so SvelteKit alias # imports resolve to the same file node IDs the extractor creates (#701). - resolved_alias = None - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) - break + resolved_alias = _resolve_tsconfig_alias(raw, aliases) if resolved_alias is not None: resolved_alias = _resolve_js_module_path(resolved_alias) node_id = _make_id(str(resolved_alias)) @@ -4184,12 +4354,7 @@ def extract_svelte(path: Path) -> dict: node_id = _make_id(str(resolved)) stub_source_file = str(resolved) else: - resolved_alias = None - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) - break + resolved_alias = _resolve_tsconfig_alias(raw, aliases) if resolved_alias is not None: node_id = _make_id(str(resolved_alias)) stub_source_file = str(resolved_alias) @@ -4254,12 +4419,7 @@ def extract_astro(path: Path) -> dict: node_id = _make_id(str(resolved)) stub_source_file = str(resolved) else: - resolved_alias = None - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) - break + resolved_alias = _resolve_tsconfig_alias(raw, aliases) if resolved_alias is not None: resolved_alias = _resolve_js_module_path(resolved_alias) node_id = _make_id(str(resolved_alias)) @@ -4320,12 +4480,7 @@ def extract_astro(path: Path) -> dict: node_id = _make_id(str(resolved)) stub_source_file = str(resolved) else: - resolved_alias = None - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) - break + resolved_alias = _resolve_tsconfig_alias(raw, aliases) if resolved_alias is not None: node_id = _make_id(str(resolved_alias)) stub_source_file = str(resolved_alias) @@ -4436,12 +4591,7 @@ def extract_vue(path: Path) -> dict: node_id = _make_id(str(resolved)) stub_source_file = str(resolved) else: - resolved_alias = None - for alias_prefix, alias_base in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) - break + resolved_alias = _resolve_tsconfig_alias(raw, aliases) if resolved_alias is not None: resolved_alias = _resolve_js_module_path(resolved_alias) node_id = _make_id(str(resolved_alias)) @@ -9494,6 +9644,23 @@ def _key(label: str) -> str: }) +# Register the cross-file, language-specific member-call resolvers into the shared +# registry (framework lives in graphify.resolver_registry). A new language plugs in +# by adding one register() call below — no edits to extract()'s body. Order +# preserved from the prior inlined wiring: Swift (#1356) before Python (#1446). +register_language_resolver( + LanguageResolver("swift_member_calls", frozenset({".swift"}), _resolve_swift_member_calls) +) +register_language_resolver( + LanguageResolver("python_member_calls", frozenset({".py"}), _resolve_python_member_calls) +) +# Ruby type-aware member-call resolution (Class.new + typed var.method). Lives in +# graphify.ruby_resolution; registered here as a second consumer of the framework. +register_language_resolver( + LanguageResolver("ruby_member_calls", frozenset({".rb"}), resolve_ruby_member_calls) +) + + def extract_objc(path: Path) -> dict: """Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files.""" try: @@ -13293,7 +13460,14 @@ def extract( # main_run -- splitting the symbol into AST/semantic ghosts (#1096). Relativize # the symbol prefix the same way, gated by source_file so two files sharing a # prefix can't cross-contaminate. Keyed by resolved path -> (old_pref, new_pref). - prefix_remap: dict[Path, tuple[str, str]] = {} + # Each file maps from up to TWO old prefixes — the input-form prefix + # _file_node_id(path) and the absolute-resolved-form prefix + # _file_node_id(path.resolve()). Alias/workspace imports resolve specifiers + # through .resolve(), so their edge targets are keyed off the ABSOLUTE form; + # when inputs are relative the two forms differ and absolute-derived targets + # would otherwise orphan (#1529). Stored as a list so the symbol-prefix remap + # below can try both (identical forms collapse to one — a no-op). + prefix_remap: dict[Path, list[tuple[str, str]]] = {} for path in paths: old_id = _make_id(str(path)) try: @@ -13306,9 +13480,21 @@ def extract( new_id = _file_node_id(rel) if old_id != new_id: id_remap[old_id] = new_id + # Also register the absolute-resolved form of the file-level id so + # alias/workspace import targets (resolved via .resolve()) remap to + # canonical instead of orphaning (#1529). + old_id_abs = _make_id(str(path.resolve())) + if old_id_abs != new_id: + id_remap[old_id_abs] = new_id + old_prefs: list[tuple[str, str]] = [] old_pref = _file_node_id(path) if old_pref != new_id: - prefix_remap[path.resolve()] = (old_pref, new_id) + old_prefs.append((old_pref, new_id)) + old_pref_abs = _file_node_id(path.resolve()) + if old_pref_abs != new_id and old_pref_abs != old_pref: + old_prefs.append((old_pref_abs, new_id)) + if old_prefs: + prefix_remap[path.resolve()] = old_prefs if id_remap: for n in all_nodes: if n.get("id") in id_remap: @@ -13336,12 +13522,16 @@ def extract( continue if entry is None: continue - old_pref, new_pref = entry nid = n.get("id", "") - if nid.startswith(old_pref + "_"): - new_nid = new_pref + nid[len(old_pref):] - if new_nid != nid: - sym_remap[nid] = new_nid + # Try both the input-form and absolute-form prefixes for this file + # (#1529). source_file gating above already prevents cross-file + # contamination, so the first matching prefix wins. + for old_pref, new_pref in entry: + if nid.startswith(old_pref + "_"): + new_nid = new_pref + nid[len(old_pref):] + if new_nid != nid: + sym_remap[nid] = new_nid + break if sym_remap: for n in all_nodes: if n.get("id") in sym_remap: @@ -13528,26 +13718,12 @@ def _has_import_evidence(candidate_id: str) -> bool: "weight": 1.0, }) - # Cross-file Swift member-call resolution (#1356). Runs after the shared call - # pass so node ids/caller_nids are final; additive (only receiver-typed calls - # the shared pass skipped), with a single-definition god-node guard. - swift_paths = [p for p in paths if p.suffix == ".swift"] - if swift_paths: - try: - _resolve_swift_member_calls(per_file, all_nodes, all_edges) - except Exception as exc: - import logging - logging.getLogger(__name__).warning("Swift member-call resolution failed, skipping: %s", exc) - - # Cross-file Python qualified class-method resolution (#1446). Same shape as the - # Swift pass: additive, runs after id-disambiguation, single-definition guard. - py_paths = [p for p in paths if p.suffix == ".py"] - if py_paths: - try: - _resolve_python_member_calls(per_file, all_nodes, all_edges) - except Exception as exc: - import logging - logging.getLogger(__name__).warning("Python member-call resolution failed, skipping: %s", exc) + # Cross-file, language-specific member-call resolution. Runs after the shared + # call pass so node ids/caller_nids are final; each pass is additive (only the + # receiver-typed/qualified calls the shared pass skipped) with its own + # single-definition god-node guard. Registered in graphify.resolver_registry so + # a new language plugs in without editing this body (#1356 Swift, #1446 Python). + run_language_resolvers(paths, per_file, all_nodes, all_edges) # Relativize source_file fields so paths are portable across machines (#555) for item in all_nodes + all_edges: diff --git a/graphify/llm.py b/graphify/llm.py index 396f9dc1c..c0d2efa25 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -1905,7 +1905,7 @@ def _call_llm( import anthropic except ImportError as exc: raise ImportError(_backend_pkg_hint("anthropic", "anthropic")) from exc - client = anthropic.Anthropic(api_key=key, base_url=cfg["base_url"], max_retries=_resolve_max_retries()) + client = anthropic.Anthropic(api_key=key, base_url=cfg["base_url"], timeout=_resolve_api_timeout(), max_retries=_resolve_max_retries()) resp = client.messages.create( model=mdl, max_tokens=max_tokens, @@ -1988,7 +1988,7 @@ def _call_llm( from openai import OpenAI except ImportError as exc: raise ImportError(_backend_pkg_hint("openai", "openai")) from exc - client = OpenAI(api_key=key, base_url=cfg["base_url"], max_retries=_resolve_max_retries()) + client = OpenAI(api_key=key, base_url=cfg["base_url"], timeout=_resolve_api_timeout(), max_retries=_resolve_max_retries()) kwargs: dict = { "model": mdl, "messages": [{"role": "user", "content": prompt}], diff --git a/graphify/resolver_registry.py b/graphify/resolver_registry.py new file mode 100644 index 000000000..b17478a78 --- /dev/null +++ b/graphify/resolver_registry.py @@ -0,0 +1,85 @@ +"""Registry for cross-file, language-specific resolution passes. + +Some call/reference edges can only be resolved with language-specific knowledge +(receiver typing, qualified member calls, framework conventions). Historically +these ran as a hand-wired sequence of suffix-gated +``if _paths: try: _resolve_(...)`` blocks at the tail of +``extract.extract()``. That pattern is the de-facto extension point for +per-language resolution; this module formalizes it so a new language plugs in by +registering one ``LanguageResolver`` instead of editing ``extract()``'s body. + +This module deliberately knows nothing about any specific language — languages +register themselves (see ``extract.py``), keeping the dependency direction one +way (``extract`` → ``resolver_registry``) and this seam small and reviewable on +its own, separate from the multi-thousand-line ``extract`` module. +""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +_LOG = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class LanguageResolver: + """One cross-file, language-specific resolution pass. + + ``resolve`` has the signature ``(per_file, all_nodes, all_edges) -> None`` and + mutates ``all_nodes`` / ``all_edges`` in place, matching the existing + member-call resolvers. ``suffixes`` gates activation: the pass runs only when + the corpus contains at least one file with one of these extensions. + """ + + name: str + suffixes: frozenset + resolve: Callable + + +# Module-level registry, populated by callers via register(). Ordered: resolvers +# run in registration order, preserving any required sequencing between passes. +_REGISTRY: list[LanguageResolver] = [] + + +def register(resolver: LanguageResolver) -> LanguageResolver: + """Append a resolver to the global registry and return it (for inline use).""" + _REGISTRY.append(resolver) + return resolver + + +def registered_resolvers() -> list[LanguageResolver]: + """Return a copy of the registered resolvers, in registration order.""" + return list(_REGISTRY) + + +def run_language_resolvers( + paths: Sequence[Path], + per_file: list[dict], + all_nodes: list[dict], + all_edges: list[dict], + *, + resolvers: Sequence[LanguageResolver] | None = None, +) -> None: + """Run every resolver whose suffix appears in ``paths``. + + Behaviorally identical to the prior hand-wired sequence of suffix-gated, + try/except-wrapped passes: same activation rule (suffix present), same + failure handling (log a warning and continue to the next pass), same + execution order (registration order). + + ``resolvers`` defaults to the global registry; tests pass an explicit list to + exercise the driver in isolation. + """ + active = _REGISTRY if resolvers is None else resolvers + suffixes_present = {p.suffix for p in paths} + for resolver in active: + if not (resolver.suffixes & suffixes_present): + continue + try: + resolver.resolve(per_file, all_nodes, all_edges) + except Exception as exc: + _LOG.warning("%s resolution failed, skipping: %s", resolver.name, exc) diff --git a/graphify/ruby_resolution.py b/graphify/ruby_resolution.py new file mode 100644 index 000000000..d0e41dd33 --- /dev/null +++ b/graphify/ruby_resolution.py @@ -0,0 +1,125 @@ +"""Type-aware cross-file resolution for Ruby member calls. + +Ruby has no type annotations and reuses method names heavily, so resolving +``obj.method()`` by globally-unique name is both lossy (drops on collision) and +unsafe (can attach to the wrong same-named method). This resolver instead uses +the receiver's *type*, inferred at extraction time from local +``var = ClassName.new`` bindings and carried on each member-call raw_call as +``receiver_type``. + +It resolves two shapes, both at EXTRACTED (1.0) confidence and only when the +target is certain (single owning class, single owned method) — bail otherwise: + + * ``Processor.new`` -> a ``calls`` edge to the ``Processor`` class + * ``p.run`` where ``p`` is a ``Processor`` -> a ``calls`` edge to ``Processor#run`` + +Registered into graphify.resolver_registry and run by extract() after id +disambiguation, so node ids and raw_call caller_nids are final. +""" + +from __future__ import annotations + +import re +from typing import Any + + +def _key(label: str) -> str: + """Normalize a class/method label to a comparison key (drop punctuation).""" + return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower() + + +def _ruby_raw_calls(per_file: list[dict]) -> list[dict]: + calls: list[dict] = [] + for result in per_file: + if not isinstance(result, dict): + continue + for rc in result.get("raw_calls", []): + if not isinstance(rc, dict): + continue + sf = str(rc.get("source_file", "")) + if sf.endswith(".rb"): + calls.append(rc) + return calls + + +def resolve_ruby_member_calls( + per_file: list[dict], + all_nodes: list[dict], + all_edges: list[dict], +) -> None: + """Resolve Ruby ``Class.new`` and typed ``var.method`` calls by receiver type. + + Purely additive: only emits edges the shared (name-based) call pass skips + because they are member calls. Each emission requires a single owning class + (god-node guard) so an ambiguous class name resolves to nothing rather than a + wrong edge. + """ + node_by_id: dict[str, dict] = {n.get("id"): n for n in all_nodes} + + # class label key -> [class node ids]; (class_node_id, method_key) -> method id + class_def_nids: dict[str, list[str]] = {} + method_index: dict[tuple[str, str], str] = {} + for e in all_edges: + if e.get("relation") != "method": + continue + src, tgt = e.get("source"), e.get("target") + cnode = node_by_id.get(src) + if cnode is not None: + class_def_nids.setdefault(_key(cnode.get("label", "")), []).append(str(src)) + tnode = node_by_id.get(tgt) + if tnode is not None: + method_index[(str(src), _key(tnode.get("label", "")))] = str(tgt) + for k in list(class_def_nids): + class_def_nids[k] = sorted(set(class_def_nids[k])) + + existing_pairs = {(e.get("source"), e.get("target")) for e in all_edges} + + def _unique_class(name: str) -> str | None: + nids = class_def_nids.get(_key(name), []) + return nids[0] if len(nids) == 1 else None + + def _emit(caller: str, target: str, rc: dict[str, Any]) -> None: + if not caller or not target or caller == target: + return + if (caller, target) in existing_pairs: + return + existing_pairs.add((caller, target)) + all_edges.append({ + "source": caller, + "target": target, + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": rc.get("source_file", ""), + "source_location": rc.get("source_location"), + "weight": 1.0, + }) + + for rc in _ruby_raw_calls(per_file): + if not rc.get("is_member_call"): + continue + caller = str(rc.get("caller_nid", "")) + callee = rc.get("callee") + if not caller or not callee: + continue + + # `Processor.new` -> instantiation edge to the class. + receiver = rc.get("receiver") + if callee == "new" and receiver and str(receiver)[:1].isupper(): + class_nid = _unique_class(str(receiver)) + if class_nid is not None: + _emit(caller, class_nid, rc) + continue + + # `p.run` where p's type is known -> edge to that class's method. + receiver_type = rc.get("receiver_type") + if not receiver_type: + continue + class_nid = _unique_class(str(receiver_type)) + if class_nid is None: + continue + method_nid = method_index.get((class_nid, _key(str(callee)))) + if method_nid is None: + continue + _emit(caller, method_nid, rc) diff --git a/pyproject.toml b/pyproject.toml index 35e78159b..e6c68b83d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,6 @@ dev = [ "pytest>=9.0.3", "pytest-cov>=7.1.0", "ruff>=0.15.13", - "safety>=3.7.0", "setuptools>=82.0.1", "wheel>=0.47.0", "tomli>=2.0 ; python_version < '3.11'", diff --git a/tests/.DS_Store b/tests/.DS_Store deleted file mode 100644 index a5adcfce1..000000000 Binary files a/tests/.DS_Store and /dev/null differ diff --git a/tests/test_cache.py b/tests/test_cache.py index b13f08507..730265be4 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -420,3 +420,93 @@ def test_save_cached_in_root_symlink_keeps_symlink_name(tmp_path): f"cache must store symlink name, not resolved target; got " f"{on_disk['nodes'][0]['source_file']!r}" ) + + +def test_semantic_prune_removes_orphan_entries(tmp_path): + """Changing a file's content leaves the old content-hash entry orphaned; + pruning against the new live hash removes the stale entry and keeps the + current one.""" + from graphify.cache import prune_semantic_cache + + f = tmp_path / "doc.md" + f.write_text("# A\n\nContent A.\n") + h_a = file_hash(f, tmp_path) + save_cached(f, {"nodes": [{"id": "a"}], "edges": []}, root=tmp_path, kind="semantic") + + f.write_text("# B\n\nContent B.\n") + h_b = file_hash(f, tmp_path) + save_cached(f, {"nodes": [{"id": "b"}], "edges": []}, root=tmp_path, kind="semantic") + + semantic_dir = cache_dir(tmp_path, "semantic") + assert (semantic_dir / f"{h_a}.json").exists() + assert (semantic_dir / f"{h_b}.json").exists() + + pruned = prune_semantic_cache(tmp_path, {h_b}) + assert pruned == 1 + assert not (semantic_dir / f"{h_a}.json").exists() + assert (semantic_dir / f"{h_b}.json").exists() + + +def test_semantic_prune_keeps_live_unchanged_entries(tmp_path): + """Pruning against the FULL live set must keep every live entry — guards + the trap of pruning against an incremental changed-subset, which would + delete all unchanged docs' valid entries.""" + from graphify.cache import prune_semantic_cache + + live_hashes = set() + for i in range(5): + f = tmp_path / f"doc{i}.md" + f.write_text(f"# Doc {i}\n\nBody {i}.\n") + save_cached(f, {"nodes": [{"id": str(i)}], "edges": []}, root=tmp_path, kind="semantic") + live_hashes.add(file_hash(f, tmp_path)) + + semantic_dir = cache_dir(tmp_path, "semantic") + assert len(list(semantic_dir.glob("*.json"))) == 5 + + pruned = prune_semantic_cache(tmp_path, live_hashes) + assert pruned == 0 + assert len(list(semantic_dir.glob("*.json"))) == 5 + + +def test_semantic_prune_handles_deleted_file(tmp_path): + """An entry for a file that no longer exists (dropped from the live set) is + pruned.""" + from graphify.cache import prune_semantic_cache + + f = tmp_path / "gone.md" + f.write_text("# Gone\n\nWill be deleted.\n") + h = file_hash(f, tmp_path) + save_cached(f, {"nodes": [{"id": "g"}], "edges": []}, root=tmp_path, kind="semantic") + semantic_dir = cache_dir(tmp_path, "semantic") + assert (semantic_dir / f"{h}.json").exists() + + f.unlink() + # Live set is empty: the file is gone, so its entry must be pruned. + pruned = prune_semantic_cache(tmp_path, set()) + assert pruned == 1 + assert not (semantic_dir / f"{h}.json").exists() + + +def test_semantic_prune_ignores_ast_and_tmp(tmp_path): + """Prune touches only cache/semantic/*.json: AST entries and atomic-write + *.tmp temporaries are left untouched.""" + from graphify.cache import prune_semantic_cache + + f = tmp_path / "doc.md" + f.write_text("# Doc\n\nBody.\n") + # AST entry (different subtree) must survive. + save_cached(f, {"nodes": [{"id": "ast"}], "edges": []}, root=tmp_path, kind="ast") + ast_dir = cache_dir(tmp_path, "ast") + assert len(list(ast_dir.glob("*.json"))) == 1 + + # A semantic orphan .json (to be pruned) plus a .tmp temporary (to survive). + semantic_dir = cache_dir(tmp_path, "semantic") + (semantic_dir / "deadbeef.json").write_text('{"nodes": [], "edges": []}') + tmp_entry = semantic_dir / "deadbeef.tmp" + tmp_entry.write_text("partial") + + pruned = prune_semantic_cache(tmp_path, set()) + assert pruned == 1 + assert not (semantic_dir / "deadbeef.json").exists() + assert tmp_entry.exists(), "*.tmp temporaries must not be swept" + assert len(list(ast_dir.glob("*.json"))) == 1, "AST entries must not be touched" diff --git a/tests/test_export.py b/tests/test_export.py index 6a1a439d1..fb5481f22 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -82,6 +82,23 @@ def test_to_graphml_has_community_attribute(): content = out.read_text() assert "community" in content +def test_to_graphml_tolerates_none_attribute_values(): + """nx.write_graphml raises ValueError on a None attribute value; to_graphml + must coerce None -> "" so a node/edge with a null field still exports (#1502).""" + G = make_graph() + communities = cluster(G) + # Inject a None-valued attribute on one node and one edge. + a_node = next(iter(G.nodes())) + G.nodes[a_node]["nullable_field"] = None + if G.number_of_edges(): + u, v = next(iter(G.edges())) + G.edges[u, v]["nullable_field"] = None + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.graphml" + to_graphml(G, communities, str(out)) # must not raise + content = out.read_text() + assert " str: assert ("run", "Payload", "parameter_type") in reference_contexts assert ("run", "Result", "return_type") in reference_contexts assert ("run", "Payload", "generic_arg") in reference_contexts + + +# ── #1531: tsconfig path-alias fallback targets ────────────────────────────── + + +def test_tsconfig_alias_resolves_second_target_when_first_missing(tmp_path: Path): + # tsc tries each `paths` target in declared order until one resolves on disk. + # The file lives only at the SECOND target, so keeping only the first entry + # (#1531) dropped the edge. + _write( + tmp_path / "tsconfig.json", + json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"$lib/*": ["generated/*", "src/lib/*"]}}}), + ) + target = _write(tmp_path / "src/lib/utils.ts", "export const helper = 1\n") + importer = _write( + tmp_path / "src/routes/page.ts", + "import { helper } from '$lib/utils'\nconsole.log(helper)\n", + ) + + result = _extract_for([target, importer], tmp_path) + + assert _has_edge(result, "src/routes/page.ts", "src/lib/utils.ts") + + +def test_tsconfig_alias_first_target_wins_when_both_exist(tmp_path: Path): + # When the file exists at BOTH targets, tsc resolves to the FIRST. The edge + # must target the generated/ copy, not src/lib. + _write( + tmp_path / "tsconfig.json", + json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"$lib/*": ["generated/*", "src/lib/*"]}}}), + ) + first = _write(tmp_path / "generated/utils.ts", "export const helper = 1\n") + second = _write(tmp_path / "src/lib/utils.ts", "export const helper = 2\n") + importer = _write( + tmp_path / "src/routes/page.ts", + "import { helper } from '$lib/utils'\nconsole.log(helper)\n", + ) + + result = _extract_for([first, second, importer], tmp_path) + + assert _has_edge(result, "src/routes/page.ts", "generated/utils.ts") + assert not _has_edge(result, "src/routes/page.ts", "src/lib/utils.ts") + + +def test_tsconfig_alias_none_exist_creates_no_false_edge(tmp_path: Path): + # The file exists at neither target; no concrete imports_from edge to either + # candidate may be fabricated (it stays an external/phantom target). + _write( + tmp_path / "tsconfig.json", + json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"$lib/*": ["generated/*", "src/lib/*"]}}}), + ) + other = _write(tmp_path / "src/routes/other.ts", "export const x = 1\n") + importer = _write( + tmp_path / "src/routes/page.ts", + "import { helper } from '$lib/utils'\nconsole.log(helper)\n", + ) + + result = _extract_for([other, importer], tmp_path) + + assert not _has_edge(result, "src/routes/page.ts", "generated/utils.ts") + assert not _has_edge(result, "src/routes/page.ts", "src/lib/utils.ts") + + +# ── #1529: alias/workspace import targets orphaned by the full-path migration ── + + +def test_alias_import_edge_resolves_with_relative_input_paths(tmp_path, monkeypatch): + # CRUCIAL: pass RELATIVE input paths (chdir into the project). Alias imports + # resolve specifiers through .resolve(), so the import-target id is keyed off + # the ABSOLUTE path; with relative inputs the id_remap (keyed on the input + # form) never rewrote it -> orphan -> dropped edge (#1529). Absolute/tmp_path + # inputs hide the bug because the two forms coincide. + _write( + tmp_path / "tsconfig.json", + json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"@/*": ["src/*"]}}}), + ) + _write(tmp_path / "src/lib/utils.ts", "export function formatDate(d) { return d }\n") + _write( + tmp_path / "src/components/Button.tsx", + "import { formatDate } from '@/lib/utils'\nexport function Button() { return formatDate(1) }\n", + ) + + monkeypatch.chdir(tmp_path) + rel_paths = [Path("src/lib/utils.ts"), Path("src/components/Button.tsx")] + result = extract(rel_paths, cache_root=Path(".")) + + node_ids = {n["id"] for n in result["nodes"]} + target_id = _file_node_id(Path("src/lib/utils.ts")) + + # The file-level imports_from edge must target the REAL utils file node (a node + # that exists in the graph), not an orphan keyed by an absolute prefix. + assert _has_edge(result, "src/components/Button.tsx", "src/lib/utils.ts") + assert target_id in node_ids + import_targets = { + e["target"] + for e in result["edges"] + if e["relation"] == "imports_from" and e["source"] == _file_node_id(Path("src/components/Button.tsx")) + } + assert target_id in import_targets + # No surviving edge target may carry an absolute-path prefix from tmp_path. + abs_prefix = _file_node_id(Path("src/lib/utils.ts").resolve()) + assert all(not t.startswith(abs_prefix + "_") and t != abs_prefix for t in import_targets) + + # The named-symbol edge to formatDate must resolve to the real symbol node too. + assert _has_symbol_edge(result, "src/components/Button.tsx", "src/lib/utils.ts", "formatDate") diff --git a/tests/test_language_resolvers.py b/tests/test_language_resolvers.py new file mode 100644 index 000000000..787c1d505 --- /dev/null +++ b/tests/test_language_resolvers.py @@ -0,0 +1,74 @@ +"""Tests for the language resolver registry (graphify.resolver_registry). + +The registry formalizes the previously hand-wired, suffix-gated cross-file +resolution passes. These tests pin its contract so future languages can be +registered with confidence: gating by suffix, in-order execution, in-place +mutation, and fault isolation (a failing pass logs and is skipped, never +aborting the build or blocking later passes). +""" + +from __future__ import annotations + +from pathlib import Path + +from graphify.resolver_registry import ( + LanguageResolver, + registered_resolvers, + run_language_resolvers, +) + + +def _make_resolver(name: str, suffix: str, log: list[str]) -> LanguageResolver: + def _resolve(per_file, all_nodes, all_edges): + log.append(name) + return LanguageResolver(name, frozenset({suffix}), _resolve) + + +def test_default_registry_contains_swift_then_python() -> None: + # Importing extract registers its resolvers into the shared registry. Order + # matters: it preserves the prior inlined wiring (Swift before Python). + import graphify.extract # noqa: F401 (registers resolvers on import) + + names = [r.name for r in registered_resolvers()] + assert "swift_member_calls" in names + assert "python_member_calls" in names + assert names.index("swift_member_calls") < names.index("python_member_calls") + + +def test_resolver_runs_only_when_suffix_present() -> None: + log: list[str] = [] + resolvers = [_make_resolver("ruby", ".rb", log), _make_resolver("go", ".go", log)] + run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers) + assert log == ["ruby"] # go skipped: no .go file present + + +def test_resolvers_run_in_given_order() -> None: + log: list[str] = [] + resolvers = [_make_resolver("first", ".rb", log), _make_resolver("second", ".rb", log)] + run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers) + assert log == ["first", "second"] + + +def test_failing_resolver_is_isolated() -> None: + log: list[str] = [] + + def _boom(per_file, all_nodes, all_edges): + raise RuntimeError("resolver blew up") + + resolvers = [ + LanguageResolver("boom", frozenset({".rb"}), _boom), + _make_resolver("after", ".rb", log), + ] + # Must not raise, and the later resolver still runs. + run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers) + assert log == ["after"] + + +def test_resolver_mutates_edges_in_place() -> None: + def _add_edge(per_file, all_nodes, all_edges): + all_edges.append({"source": "x", "target": "y", "relation": "calls"}) + + resolvers = [LanguageResolver("adder", frozenset({".rb"}), _add_edge)] + edges: list[dict] = [] + run_language_resolvers([Path("a.rb")], [], [], edges, resolvers=resolvers) + assert edges == [{"source": "x", "target": "y", "relation": "calls"}] diff --git a/tests/test_llm_backends.py b/tests/test_llm_backends.py index acb04e956..79f64027e 100644 --- a/tests/test_llm_backends.py +++ b/tests/test_llm_backends.py @@ -1028,3 +1028,62 @@ def create(self, **_): "user msg", temperature=0, max_completion_tokens=4096, backend="kimi", ) assert ctor_kwargs.get("max_retries", 0) >= 5, ctor_kwargs + + +def test_call_llm_claude_client_built_with_timeout_and_retries(monkeypatch): + """The secondary dispatch path (_call_llm, used by the dedup tiebreaker) + must build its Anthropic client with both timeout and max_retries, matching + the primary extraction path — #1442. Previously _call_llm passed neither + (then only max_retries), so GRAPHIFY_API_TIMEOUT was silently ignored here.""" + import sys + import types + + ctor_kwargs = {} + + class _FakeMessages: + def create(self, **_): + return types.SimpleNamespace(content=[types.SimpleNamespace(text="ok")]) + + class _FakeAnthropic: + def __init__(self, *_, **kwargs): + ctor_kwargs.update(kwargs) + self.messages = _FakeMessages() + + fake_module = types.ModuleType("anthropic") + fake_module.Anthropic = _FakeAnthropic + monkeypatch.setitem(sys.modules, "anthropic", fake_module) + monkeypatch.setattr(llm, "_get_backend_api_key", lambda _b: "fake-key") + monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "1") + monkeypatch.delenv("GRAPHIFY_MAX_RETRIES", raising=False) + + assert llm._call_llm("hi", backend="claude") == "ok" + assert ctor_kwargs.get("timeout") == 1.0, ctor_kwargs + assert ctor_kwargs.get("max_retries", 0) >= 5, ctor_kwargs + + +def test_call_llm_openai_compat_client_built_with_timeout_and_retries(monkeypatch): + """Same #1442 fix for the OpenAI-compatible branch of _call_llm.""" + import sys + import types + + ctor_kwargs = {} + + class _FakeOpenAI: + def __init__(self, *_, **kwargs): + ctor_kwargs.update(kwargs) + self.chat = self + self.completions = self + + def create(self, **_): + return _fake_openai_response("ok", finish_reason="stop", completion_tokens=1) + + fake_module = types.ModuleType("openai") + fake_module.OpenAI = _FakeOpenAI + monkeypatch.setitem(sys.modules, "openai", fake_module) + monkeypatch.setattr(llm, "_get_backend_api_key", lambda _b: "fake-key") + monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "1") + monkeypatch.delenv("GRAPHIFY_MAX_RETRIES", raising=False) + + llm._call_llm("hi", backend="kimi") + assert ctor_kwargs.get("timeout") == 1.0, ctor_kwargs + assert ctor_kwargs.get("max_retries", 0) >= 5, ctor_kwargs diff --git a/tests/test_reflect.py b/tests/test_reflect.py index c30f3949d..051617207 100644 --- a/tests/test_reflect.py +++ b/tests/test_reflect.py @@ -478,6 +478,27 @@ def test_cli_save_result_rejects_bad_outcome(tmp_path): assert "great" in (r.stderr + r.stdout) +def test_cli_save_result_reads_answer_from_file(tmp_path): + """--answer-file lets callers pass a long/multiline answer via a file instead + of a fragile inline arg (Windows/PowerShell quoting), #1502.""" + ans = tmp_path / "answer.txt" + ans.write_text("line one\nline two with a \"quote\"\n", encoding="utf-8") + r = _run(["save-result", "--question", "how does auth work?", + "--answer-file", str(ans), "--outcome", "useful"], tmp_path) + assert r.returncode == 0, r.stderr + docs = list((tmp_path / "graphify-out" / "memory").glob("*.md")) + assert docs, "save-result wrote no memory doc" + body = docs[0].read_text(encoding="utf-8") + assert "line one" in body and "line two" in body + + +def test_cli_save_result_requires_answer_or_answer_file(tmp_path): + """Neither --answer nor --answer-file -> clean argparse error, not a crash.""" + r = _run(["save-result", "--question", "q", "--outcome", "useful"], tmp_path) + assert r.returncode != 0 + assert "--answer" in (r.stderr + r.stdout) + + def test_cli_reflect_cold_start_writes_empty_lessons(tmp_path): """First run with no graphify-out/memory/ still succeeds and writes a valid doc.""" r = _run(["reflect"], tmp_path) diff --git a/tests/test_ruby_resolution.py b/tests/test_ruby_resolution.py new file mode 100644 index 000000000..d11b1cf8b --- /dev/null +++ b/tests/test_ruby_resolution.py @@ -0,0 +1,195 @@ +"""TDD specs for type-aware Ruby call-graph resolution. + +These drive the "improved Ruby graph" work: + * member calls capture their receiver (extraction) + * `var = ClassName.new` local bindings give the receiver a type (extraction) + * the cross-file resolver turns `var.method` into a precise edge BY TYPE, + not by globally-unique name — so it survives name collisions and never + emits a false positive when the type is unknown (resolution) + * `require_relative` links files (resolution) + +Every resolved edge must be EXTRACTED (1.0) confidence: resolve only when +certain, bail otherwise. +""" + +from __future__ import annotations + +from pathlib import Path + +from graphify.extract import extract, extract_ruby + + +# ── helpers ──────────────────────────────────────────────────────────────────── + + +def _write(tmp_path: Path, name: str, body: str) -> Path: + p = tmp_path / name + p.write_text(body) + return p + + +def _raw_calls(result: dict) -> list[dict]: + return result.get("raw_calls", []) + + +def _find_raw_call(result: dict, callee: str) -> dict | None: + for rc in _raw_calls(result): + if rc.get("callee") == callee: + return rc + return None + + +def _labels(nodes: list[dict]) -> dict[str, str]: + return {n["id"]: str(n.get("label", "")) for n in nodes} + + +def _has_call_edge(graph: dict, src_label_sub: str, tgt_label_sub: str) -> dict | None: + """Return the `calls` edge whose source/target labels contain the given + substrings, or None.""" + labels = _labels(graph["nodes"]) + for e in graph["edges"]: + if e.get("relation") != "calls": + continue + s = labels.get(e.get("source"), "") + t = labels.get(e.get("target"), "") + if src_label_sub in s and tgt_label_sub in t: + return e + return None + + +HELPER_RB = """\ +def transform(data) + data.upcase +end + +class Processor + def run(items) + items.map { |i| transform(i) } + end +end +""" + +MAIN_RB = """\ +require_relative "helper" + +def handle(values) + transform(values) +end + +def process_all(items) + p = Processor.new + p.run(items) +end +""" + +WORKER_RB = """\ +class Worker + def run(jobs) + jobs.each { |j| j } + end +end +""" + + +# ── extraction level ─────────────────────────────────────────────────────────── + + +def test_member_call_captures_receiver(tmp_path: Path) -> None: + main = _write(tmp_path, "main.rb", MAIN_RB) + rc = _find_raw_call(extract_ruby(main), "run") + assert rc is not None, "p.run should produce a raw_call with callee 'run'" + assert rc["is_member_call"] is True + assert rc["receiver"] == "p" + + +def test_local_binding_gives_receiver_a_type(tmp_path: Path) -> None: + main = _write(tmp_path, "main.rb", MAIN_RB) + rc = _find_raw_call(extract_ruby(main), "run") + assert rc is not None + # `p = Processor.new` in the same method => p has type Processor. + assert rc.get("receiver_type") == "Processor" + + +def test_ambiguous_binding_yields_no_type(tmp_path: Path) -> None: + main = _write( + tmp_path, + "main.rb", + """\ +def process_all(items) + p = Processor.new + p = Worker.new + p.run(items) +end +""", + ) + rc = _find_raw_call(extract_ruby(main), "run") + assert rc is not None + # reassigned to a different class => not certain => no type attached. + assert rc.get("receiver_type") is None + + +# ── resolution level ─────────────────────────────────────────────────────────── + + +def test_resolves_member_call_by_type(tmp_path: Path) -> None: + _write(tmp_path, "helper.rb", HELPER_RB) + main = _write(tmp_path, "main.rb", MAIN_RB) + graph = extract([main, tmp_path / "helper.rb"], cache_root=tmp_path, parallel=False) + edge = _has_call_edge(graph, "process_all", "run") + assert edge is not None, "process_all should resolve a call to Processor#run" + assert edge["confidence"] == "EXTRACTED" + + +def test_resolution_is_type_based_not_name_luck(tmp_path: Path) -> None: + """The differentiator: adding an unrelated Worker#run must NOT break the edge. + + Name-match resolvers drop this (two `run` definitions => ambiguous). A + type-based resolver keeps resolving p.run -> Processor#run, and never points + it at Worker#run. + """ + _write(tmp_path, "helper.rb", HELPER_RB) + _write(tmp_path, "worker.rb", WORKER_RB) + main = _write(tmp_path, "main.rb", MAIN_RB) + graph = extract( + [main, tmp_path / "helper.rb", tmp_path / "worker.rb"], + cache_root=tmp_path, + parallel=False, + ) + to_processor_run = _has_call_edge(graph, "process_all", "run") + assert to_processor_run is not None, "edge must survive the name collision" + assert to_processor_run["confidence"] == "EXTRACTED" + # And it must be the RIGHT run: the target must be owned by Processor, not Worker. + labels = _labels(graph["nodes"]) + tgt_id = to_processor_run["target"] + # the method node id is prefixed by its owning class (helper_processor_run) + assert "processor" in tgt_id.lower(), f"expected Processor#run, got {tgt_id}" + assert "worker" not in tgt_id.lower() + + +def test_no_false_positive_when_type_unknown(tmp_path: Path) -> None: + """A member call on a receiver with no known type must NOT be resolved.""" + _write(tmp_path, "helper.rb", HELPER_RB) + main = _write( + tmp_path, + "main.rb", + """\ +require_relative "helper" + +def process_all(thing) + thing.run(1) +end +""", + ) + graph = extract([main, tmp_path / "helper.rb"], cache_root=tmp_path, parallel=False) + # `thing` is a parameter of unknown type => no precise target => no edge. + assert _has_call_edge(graph, "process_all", "run") is None + + +def test_class_new_creates_instantiation_edge(tmp_path: Path) -> None: + """`p = Processor.new` should link the caller to the Processor class.""" + _write(tmp_path, "helper.rb", HELPER_RB) + main = _write(tmp_path, "main.rb", MAIN_RB) + graph = extract([main, tmp_path / "helper.rb"], cache_root=tmp_path, parallel=False) + edge = _has_call_edge(graph, "process_all", "Processor") + assert edge is not None, "Processor.new should resolve a call to the Processor class" + assert edge["confidence"] == "EXTRACTED" diff --git a/tests/test_skillgen.py b/tests/test_skillgen.py index 25b40a914..be404c641 100644 --- a/tests/test_skillgen.py +++ b/tests/test_skillgen.py @@ -601,8 +601,36 @@ def test_always_on_roundtrip_is_byte_faithful(): graphify.__main__, so the packaged markdown must round-trip exactly or those contracts silently change. """ + # The guard passes with zero problems: every always-on block reproduces its + # frozen baseline, with the agents-md block allowed exactly the #1530 + # sanctioned substitution recorded in gen.ALWAYS_ON_SANCTIONED_EDITS. problems = gen.always_on_roundtrip() - assert problems == [], "\n".join(problems) + assert problems == [] + + rendered_agents = next( + a.content + for a in gen.render_always_on() + if a.path == "graphify/always_on/agents-md.md" + ) + old_instruction = ( + "When the user types `/graphify`, invoke the `skill` tool with " + '`skill: "graphify"` before doing anything else.' + ) + new_instruction = ( + "When the user types `/graphify`, use the installed graphify skill or instructions " + "before doing anything else." + ) + # The sanctioned-edit registry holds exactly this single old->new substitution. + assert gen.ALWAYS_ON_SANCTIONED_EDITS["_AGENTS_MD_SECTION"] == ( + (old_instruction, new_instruction), + ) + baseline_agents = gen._always_on_constants(gen.ALWAYS_ON_BASELINE_REF)["_AGENTS_MD_SECTION"] + # The ONLY divergence from the frozen baseline is the sanctioned sentence — + # any other byte drift would have surfaced as a problem above. + assert old_instruction in baseline_agents + assert baseline_agents.replace(old_instruction, new_instruction) == rendered_agents + assert "`skill` tool" not in rendered_agents + assert 'skill: "graphify"' not in rendered_agents def test_extracted_constants_equal_the_packaged_always_on_files(): diff --git a/tools/.DS_Store b/tools/.DS_Store deleted file mode 100644 index a188808df..000000000 Binary files a/tools/.DS_Store and /dev/null differ diff --git a/tools/skillgen/expected/graphify__always_on__agents-md.md b/tools/skillgen/expected/graphify__always_on__agents-md.md index 20cff728b..6511cd1dd 100644 --- a/tools/skillgen/expected/graphify__always_on__agents-md.md +++ b/tools/skillgen/expected/graphify__always_on__agents-md.md @@ -2,7 +2,7 @@ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. -When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. diff --git a/tools/skillgen/fragments/always-on/agents-md.md b/tools/skillgen/fragments/always-on/agents-md.md index 20cff728b..6511cd1dd 100644 --- a/tools/skillgen/fragments/always-on/agents-md.md +++ b/tools/skillgen/fragments/always-on/agents-md.md @@ -2,7 +2,7 @@ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. -When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index e369dd7fc..7b198d188 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -92,6 +92,27 @@ def _v8_baseline_ref(platform_key: str) -> str: "kiro-steering": "_KIRO_STEERING", } +# Sanctioned divergences from the frozen always-on baseline above. The roundtrip +# guard deliberately does NOT track HEAD, so any *intentional* change to an +# always-on instruction block must be recorded here as an explicit, reviewable +# old -> new substitution keyed by the baseline constant. The guard applies these +# to the baseline before the byte-for-byte comparison; anything not covered here +# still fails the guard, so unrelated drift cannot slip through. Each entry is a +# one-time, audited edit to the otherwise-immutable v8 baseline. +ALWAYS_ON_SANCTIONED_EDITS: dict[str, tuple[tuple[str, str], ...]] = { + # #1530: install guidance must stay host-generic — do not tell agents to + # invoke a literal `skill` tool with `skill: "graphify"`, which is + # host-specific and not valid in every environment. + "_AGENTS_MD_SECTION": ( + ( + "When the user types `/graphify`, invoke the `skill` tool with " + '`skill: "graphify"` before doing anything else.', + "When the user types `/graphify`, use the installed graphify skill or instructions " + "before doing anything else.", + ), + ), +} + # The full six-value file_type enum (Decision A). Every rendered platform — split # or monolith — must carry exactly this enum, byte for byte. schema-singleton # guards it. @@ -561,6 +582,7 @@ def _git_show(ref: str) -> str: cwd=REPO_ROOT, capture_output=True, text=True, + encoding="utf-8", ) if result.returncode != 0: raise SystemExit(f"error: could not read {ref}: {result.stderr.strip()}") @@ -949,10 +971,18 @@ def always_on_roundtrip() -> list[str]: if const_name not in baseline: problems.append(f"could not find constant {const_name} in {ALWAYS_ON_BASELINE_REF}") continue - if rendered[path] != baseline[const_name]: + expected = baseline[const_name] + for old, new in ALWAYS_ON_SANCTIONED_EDITS.get(const_name, ()): + if old not in expected: + problems.append( + f"sanctioned edit for {const_name} no longer applies: " + f"old text not found in {ALWAYS_ON_BASELINE_REF}" + ) + expected = expected.replace(old, new) + if rendered[path] != expected: problems.append( f"always_on/{basename}.md does not reproduce {const_name} byte for byte " - f"(rendered {len(rendered[path])} chars vs baseline {len(baseline[const_name])} chars)" + f"(rendered {len(rendered[path])} chars vs baseline {len(expected)} chars)" ) return problems diff --git a/uv.lock b/uv.lock index 70e258d84..c7f712f67 100644 --- a/uv.lock +++ b/uv.lock @@ -92,19 +92,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "authlib" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "joserfc" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, -] - [[package]] name = "autograd" version = "1.8.0" @@ -148,40 +135,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" }, ] -[[package]] -name = "backports-datetime-fromisoformat" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" }, - { url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" }, - { url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" }, - { url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" }, - { url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" }, - { url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" }, - { url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" }, - { url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" }, - { url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" }, - { url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" }, - { url = "https://files.pythonhosted.org/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" }, - { url = "https://files.pythonhosted.org/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" }, -] - [[package]] name = "bandit" version = "1.9.4" @@ -959,19 +912,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] -[[package]] -name = "dparse" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" }, -] - [[package]] name = "et-xmlfile" version = "2.0.0" @@ -1150,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.0" +version = "0.9.1" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1296,7 +1236,6 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "safety" }, { name = "setuptools" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tree-sitter-hcl" }, @@ -1401,7 +1340,6 @@ dev = [ { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "ruff", specifier = ">=0.15.13" }, - { name = "safety", specifier = ">=3.7.0" }, { name = "setuptools", specifier = ">=82.0.1" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, { name = "tree-sitter-hcl", specifier = ">=1.2.0" }, @@ -1630,18 +1568,6 @@ version = "0.42.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - [[package]] name = "jiter" version = "0.14.0" @@ -1763,18 +1689,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] -[[package]] -name = "joserfc" -version = "1.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/cb/52e479f20804904f5df20ac4539d292dcecd1287aaa33cba1d1def1d9d8e/joserfc-1.6.7.tar.gz", hash = "sha256:6999fe89457069ecacd8cc797c88a805f83054dd883333fa0409f74b46479fd7", size = 232158, upload-time = "2026-05-23T01:46:44.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/e4/bcf6718b5662894c6831f46296b73cd4b1a2e90c20b6d437e20c4997388c/joserfc-1.6.7-py3-none-any.whl", hash = "sha256:9e51e4a64840aa1734a058258e80a4480e2ff2d5686e480e7c92c954a92fbe05", size = 70603, upload-time = "2026-05-23T01:46:42.129Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" @@ -2113,104 +2027,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "marshmallow" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, -] - [[package]] name = "matplotlib" version = "3.10.9" @@ -2322,63 +2138,75 @@ wheels = [ [[package]] name = "msgpack" -version = "1.1.2" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, - { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -2425,21 +2253,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] -[[package]] -name = "nltk" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, -] - [[package]] name = "nodeenv" version = "1.10.0" @@ -3337,16 +3150,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -4006,15 +3819,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruff" version = "0.15.14" @@ -4052,51 +3856,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, ] -[[package]] -name = "safety" -version = "3.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "authlib" }, - { name = "click" }, - { name = "dparse" }, - { name = "filelock" }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "marshmallow" }, - { name = "nltk" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "ruamel-yaml" }, - { name = "safety-schemas" }, - { name = "tenacity" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomlkit" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" }, -] - -[[package]] -name = "safety-schemas" -version = "0.0.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dparse" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "ruamel-yaml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" }, -] - [[package]] name = "scikit-learn" version = "1.7.2" @@ -4497,15 +4256,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, ] -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - [[package]] name = "threadpoolctl" version = "3.6.0" @@ -4666,15 +4416,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] -[[package]] -name = "tomlkit" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, -] - [[package]] name = "tqdm" version = "4.67.3" @@ -5161,10 +4902,10 @@ name = "typer" version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, + { name = "annotated-doc", marker = "python_full_version >= '3.11'" }, + { name = "click", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "shellingham", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ diff --git a/worked/.DS_Store b/worked/.DS_Store deleted file mode 100644 index 2484206f3..000000000 Binary files a/worked/.DS_Store and /dev/null differ