diff --git a/AGENTS.md b/AGENTS.md index ae467f1..e82d0ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS -This repository contains ToCode, a Python-only binary exporter. ToCode takes one binary or IDA database path and writes one source-like project directory for reverse-engineering agents. +This repository contains ToCode, a Python-only binary exporter. ToCode takes one binary, IDA database, or Android APK path and writes one source-like project directory for reverse-engineering agents. ## Scope @@ -15,7 +15,8 @@ This repository contains ToCode, a Python-only binary exporter. ToCode takes one - `src/tocode/cli.py`: command-line entry point for `tocode`. - `src/tocode/__init__.py`: package version and `export_from_binaryview()`, the in-UI Binary Ninja library entry. - `src/tocode/analysis.py`: backend-neutral binary inventory and call graph normalization. -- `src/tocode/backends/`: IDA Domain, radare2, angr, and Binary Ninja (`binja.py`) session adapters. +- `src/tocode/backends/`: IDA Domain, radare2, angr, and Binary Ninja (`binja.py`) session adapters, plus the ASC/droidasc APK backend (`asc.py`: APK set discovery, DEX inventory, worker-side class decompilation). +- `src/tocode/apk.py`: APK export pipeline (extraction, decompile pool, resource decoding, metadata); `apk_metadata.py`: manifest parsing and Android JSON documents; `apk_native.py`: extraction and background native export of every `.so`. - `src/tocode/exporter.py`: project writer, function rendering, worker-session rendering, generated export `AGENTS.md`. - `src/tocode/metadata.py`: JSON metadata and triage documents. - `src/tocode/cluster.py`: call-graph clustering. @@ -59,6 +60,17 @@ When `--tree` is passed, the export also contains: - `src/tree/**/*.c` - `function-index-tree.json` +APK input (`.apk`, `.apks`, `.xapk`) uses the ASC backend and writes instead: + +- `src/raw//**/*.java` (one file per class, folders are Java packages; no clusters, no summaries) +- `AndroidManifest.xml`, `manifest.json`, `classes.json`, `package-graph.json`, `native-libs.json` +- `functions.json`, `function-index.json`, `strings.json`, `imports.json`, `exports.json`, `sections.json`, `reachable.json`, `triage.json`, `project.json`, `export-manifest.json` (same shapes as the native export where applicable) +- `lib//*.so` (every native library, always extracted) and `native///` (a full nested ToCode export per library, all ABIs, unless `--no-native`) +- `data/apk/**` (all other entries verbatim), `data/res/**/*.xml`, `data/resources.json` +- `tocode.log`, generated `AGENTS.md` and `CLAUDE.md` + +`base.apk` merges sibling `split_*.apk` files unless `--no-splits`; bundles are unpacked and merged. `--backend` selects the native backend for the `.so` exports (`binja` is rejected). + ## Development - Prefer `uv` for local commands. @@ -74,6 +86,8 @@ When `--tree` is passed, the export also contains: - IDA Domain is the preferred backend when available. - radare2/r2pipe is a fallback backend. - angr is the optional pure-Python fallback backend (`[angr]` extra). +- ASC (`droidasc`, PyPI) is the APK/DEX backend and a core runtime dependency. It pulls androguard. `backends/asc.py` is the only module that imports it; it pre-imports the modules ASC would otherwise stub in `sys.modules` and silences androguard's loguru logging. Class decompilation is not thread-safe, so it runs in spawned worker processes (recycled in rounds; do not use `max_tasks_per_child`, it deadlocks spawn pools on some CPython builds). The big per-method/per-class/per-string JSON documents are streamed row by row (`apk_metadata.write_json_rows`), never built as one object. +- APK native libraries are exported by `apk_native.py` on a background thread, each `export_binary` in its own spawned process so a backend OOM-kill or crash only fails that library (`native-libs.json` status). The thread waits for `TOCODE_APK_NATIVE_MIN_FREE_MB` (default 1024) of free memory before starting each library so it does not starve the DEX pool. `TOCODE_WORKER_TMP_DIR` also places the unpacked `.apks` bundle. - Binary Ninja is an opt-in backend (`--backend binja`, never auto-selected). The `binaryninja` module is supplied by the Binary Ninja install (in-UI) or the remote VM, so it is not a pip dependency. `rpyc`, the client used for the diff --git a/README.md b/README.md index 71a3052..4e70536 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ToCode -ToCode exports a binary or IDA database into a source-like project tree: raw recovered C, matching assembly, function summaries, section data, optional IDA database, and metadata that coding agents can read directly. +ToCode exports a binary, IDA database, or Android APK into a source-like project tree: raw recovered C (or Java for APKs), matching assembly, function summaries, section data, optional IDA database, and metadata that coding agents can read directly. ## Why @@ -23,11 +23,22 @@ sample_decompiler/ src/raw/**/*.c src/raw/**/*.asm src/raw/**/*.summary + src/raw//**/*.java # Only for APK files (replaces the .c/.asm/.summary tree) include/*.h include/*.types.h data/*.bin data/variables.json data/variables_interesting.json + data/apk//** # Only for APK files: every non-code APK entry, verbatim + data/res//**/*.xml # Only for APK files: binary XML resources decoded to text + data/resources.json # Only for APK files: decoded resources.arsc + lib//*.so # Only for APK files: extracted native libraries + native/// # Only for APK files: full nested ToCode export per .so + AndroidManifest.xml # Only for APK files + manifest.json # Only for APK files + classes.json # Only for APK files + package-graph.json # Only for APK files (replaces cluster-graph.json) + native-libs.json # Only for APK files function-index.json functions.json types.json @@ -47,11 +58,19 @@ sample_decompiler/ | Path | Description | | --- | --- | | `src/raw` | Decompiled C-like output, assembly, and summaries. Grouped by call-graph cluster, or by the original source file/directory when the binary has debug info (DWARF). | -| `include` | Generated headers, including `*.types.h` with the structs/enums/typedefs recovered from the binary. | +| `src/raw//**/*.java` | **Only for APK files.** Decompiled Java (ASC + androguard DAD), one file per class, folders follow Java packages; inner classes are `Outer$Inner.java`. No clustering, no `.summary` files. | +| `include` | Generated headers, including `*.types.h` with the structs/enums/typedefs recovered from the binary. Not written for APK files. | | `data` | Raw section dumps and variable metadata. | +| `data/apk`, `data/res`, `data/resources.json` | **Only for APK files.** Every non-code entry of each APK in the set verbatim, `res/**/*.xml` decoded from binary XML, and the decoded `resources.arsc` tables. | +| `lib//*.so` | **Only for APK files.** Every native library found in the APK set (all ABIs), always extracted. | +| `native///` | **Only for APK files.** A complete nested ToCode export for each native library (own `AGENTS.md` with an Origin section naming the APK, `src/raw/*.c`, `functions.json`, `exports.json`, ...). Skipped with `--no-native`. | +| `AndroidManifest.xml` / `manifest.json` | **Only for APK files.** Decoded manifest and its parsed form: package, versions, SDKs, permissions, components with intent filters and exported state, application attributes, split manifests. | +| `classes.json` | **Only for APK files.** Every class with superclass, interfaces, access flags, fields, methods, source file, and Java file/line ranges. | +| `package-graph.json` | **Only for APK files.** Inter-package call graph (the APK counterpart of `cluster-graph.json`). | +| `native-libs.json` | **Only for APK files.** ABI, hash, source APK, export directory, and decompilation status of every native library. | | `types.json` | Catalog of types recovered from the binary's debug info or type library, with C declarations. | -| `*.json` | Functions (with recovered types and original source decl file/line), sections, strings, imports, exports, relocations, reachability, clusters, triage, project metadata, and export manifest. | -| `tocode.log` | Export log with checkpoint, resume, and per-function render history. | +| `*.json` | Functions (with recovered types and original source decl file/line), sections, strings, imports, exports, relocations, reachability, clusters, triage, project metadata, and export manifest. For APK files the same documents describe DEX methods, strings, framework imports, exported components/JNI methods, and reachability from manifest components; `relocations.json`, `cluster-graph.json`, and `types.json` are not written. | +| `tocode.log` | Export log with checkpoint, resume, and per-function render history. For APK files it also carries the native library export status. | | `AGENTS.md` / `CLAUDE.md` | Instructions for agents analyzing the exported binary. | | `src/tree` | Optional scanner-friendly C output when `--tree` is used. | @@ -76,6 +95,23 @@ Three backends are supported, selected with `--backend` (default `auto`, which p Other disassemblers may be added in the future. +### Android APKs + +`tocode app.apk` (also `.apks`/`.xapk` bundles) uses [ASC](https://github.com/MG1937/ASC) (`droidasc`, a core dependency) for the DEX side and the regular native backends for every shared object in the package: + +- `src/raw//.java`: one decompiled Java file per class, folders follow Java packages (no clustering, no summaries). +- `AndroidManifest.xml` + `manifest.json`: decoded and parsed manifest (permissions, components with intent filters and exported state, application attributes, split manifests). +- `classes.json`, `functions.json`, `function-index.json`, `strings.json`, `imports.json`, `exports.json` (exported components + JNI methods), `reachable.json` (from manifest components), `package-graph.json`, `sections.json`, `triage.json`. +- `lib//*.so`: every native library extracted; `native///`: a complete nested ToCode export per library (all ABIs), produced on a background thread while the DEX side decompiles. `native-libs.json` records the status of each (each library runs in its own process; one failing or being OOM-killed never fails the APK export, and the native thread waits for `TOCODE_APK_NATIVE_MIN_FREE_MB`, default 1024 MB, of free memory before each library). Pass `--no-native` to skip the native decompilation (libraries are still extracted). `--backend` picks the native backend (`auto`/`ida`/`r2`/`angr`; `binja` is not supported for APKs). +- `data/apk/**`: every other APK entry verbatim; `data/res/**/*.xml` and `data/resources.json`: decoded binary XML and `resources.arsc`. + +`base.apk` automatically merges sibling `split_*.apk` files (config and ABI splits) into the same project; `--no-splits` exports it alone. The default output directory is `_decompiler`. + +```bash +tocode base.apk # DEX + all splits + native libs (IDA/r2/angr) +tocode app.apks --no-native -j 4 # bundle, DEX/Android side only +``` + ### Using diff --git a/pyproject.toml b/pyproject.toml index b8df692..becc2c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ description = "Export compiled binaries as source-like projects for reverse engi readme = "README.md" requires-python = ">=3.10" dependencies = [ + "droidasc==0.1.1.post1", "ida-domain==0.5.0", "idapro==0.0.9", "r2pipe==1.9.8", @@ -59,6 +60,11 @@ ignore_missing_imports = true module = ["binaryninja.*"] ignore_missing_imports = true +# droidasc (ASC, the APK backend) and androguard ship no type stubs. +[[tool.mypy.overrides]] +module = ["droidasc.*", "androguard.*"] +ignore_missing_imports = true + [tool.setuptools] package-dir = { "" = "src" } diff --git a/src/tocode/apk.py b/src/tocode/apk.py new file mode 100644 index 0000000..0526dbf --- /dev/null +++ b/src/tocode/apk.py @@ -0,0 +1,1001 @@ +"""APK export pipeline (ASC backend). + +Writes one project tree for an APK, split-APK set, or ``.apks``/``.xapk`` +bundle: decompiled Java per class under ``src/raw//``, the decoded +manifest, Android metadata documents, extracted resources, and (unless +disabled) a nested native ToCode export for every shared object found. +""" + +from __future__ import annotations + +from concurrent.futures import Future, ProcessPoolExecutor, as_completed +from dataclasses import dataclass, field +import multiprocessing +import os +from pathlib import Path +import re +import shutil +import tempfile +import threading +import time +from typing import Any, Callable, Iterable +import zipfile + +from . import __version__ +from . import apk_metadata as meta +from .apk_native import ( + DEFAULT_NATIVE_MIN_FREE_MB, + NATIVE_HEARTBEAT_SECONDS, + NativeExporter, + NativeLib, + NativeOptions, + NativeRunner, + classify_native_entry, + default_native_exporter, + extract_native_libs, + native_libs_json, +) +from .backends.asc import ( + ApkSet, + AscSession, + ClassRecord, + DecompiledClass, + MethodRecord, + InlineDecompiler, + class_java_relpath, + decompile_batch_in_worker, + discover_apk_set, + init_decompile_worker, + manifest_xml, + sha256_file, +) +from .errors import ToCodeError +from .metadata import write_json, write_text_atomic +from .naming import clean_path_component +from .parallel import available_memory_mb, choose_jobs +from .progress import Progress +from .schema import ApkExportSummary + + +DECOMPILE_BATCH_SIZE = 24 +# A decompile worker holds the parsed DEX tables plus androguard's per-class +# state; budget this much RAM per worker and recycle workers periodically so +# their resident size stays bounded on long runs. +ASC_WORKER_MEMORY_MB = 768 +ASC_WORKER_BATCHES_PER_CHILD = 48 +DEX_ENTRY_RX = re.compile(r"^classes\d*\.dex$") +# A DAD member declaration at class-body indent: modifiers, return type, name, +# parameter list, optional throws, and a ``;`` only for bodiless methods. +METHOD_DECL_RX = re.compile( + r"^ (?!\{|\}|@)(?P[^;{]*?(?[\w$<>\-]+)\((?P.*)\))" + r"\s*(throws [^;{]*)?(?P;?)\s*$" +) + + +@dataclass(slots=True) +class ApkExportOptions: + out_dir: Path | None = None + jobs: int | None = None + native: bool = True + splits: bool = True + native_options: NativeOptions = field(default_factory=NativeOptions) + decode_resources: bool = True + + +@dataclass +class ApkContext: + input_path: Path + options: ApkExportOptions + progress: Progress + apk_set: ApkSet | None = None + session: AscSession | None = None + root: Path | None = None + raw_dir: Path | None = None + data_dir: Path | None = None + package: str = "" + manifest_infos: list[meta.ManifestInfo] = field(default_factory=list) + manifest_doc: dict[str, Any] = field(default_factory=dict) + manifest_xml_path: Path | None = None + entries: list[meta.ApkEntry] = field(default_factory=list) + libs: list[NativeLib] = field(default_factory=list) + native_runner: NativeRunner | None = None + resource_thread: threading.Thread | None = None + resource_errors: list[str] = field(default_factory=list) + resource_files: list[Path] = field(default_factory=list) + source_files: list[Path] = field(default_factory=list) + failures: list[tuple[str, str]] = field(default_factory=list) + reachable: dict[str, Any] = field(default_factory=dict) + worker_count: int = 1 + render_mode: str = "single" + started: float = 0.0 + bundle_dir: Path | None = None + + +def export_apk( + input_path: Path, + *, + options: ApkExportOptions | None = None, + progress: Progress | None = None, + native_exporter: NativeExporter | None = None, + decompiler_factory: Callable[[AscSession], Any] | None = None, +) -> ApkExportSummary: + options = options or ApkExportOptions() + progress = progress or Progress() + context = ApkContext( + input_path=Path(input_path).resolve(), + options=options, + progress=progress, + started=time.monotonic(), + ) + try: + _discover(context) + _prepare_root(context) + _extract_entries(context) + _start_native(context, native_exporter) + _start_resources(context) + _inventory(context) + _decompile(context, decompiler_factory) + _join_resources(context) + _write_metadata(context) + _join_native(context) + _write_final_documents(context) + except BaseException: + if context.native_runner is not None: + context.native_runner.stop() + raise + finally: + if context.session is not None: + context.session.close() + if context.bundle_dir is not None: + shutil.rmtree(context.bundle_dir, ignore_errors=True) + return _summary(context) + + +# -------------------------------------------------------------------------- +# Steps +# -------------------------------------------------------------------------- + + +def _discover(context: ApkContext) -> None: + path = context.input_path + if not path.is_file(): + raise ToCodeError(f"APK not found: {path}") + workdir = Path(tempfile.mkdtemp(prefix="tocode-apks-", dir=_bundle_tmp_root())) + apk_set = discover_apk_set(path, splits=context.options.splits, workdir=workdir) + if apk_set.workdir is not None: + context.bundle_dir = workdir + else: + shutil.rmtree(workdir, ignore_errors=True) + context.apk_set = apk_set + infos: list[meta.ManifestInfo] = [] + for apk in apk_set.apks: + try: + xml_text = manifest_xml(apk) + except ToCodeError as exc: + if apk == apk_set.primary: + raise + context.progress.log(f"warning: {exc}") + infos.append(meta.parse_manifest("", apk_name=apk.name)) + continue + info = meta.parse_manifest(xml_text, apk_name=apk.name) + infos.append(info) + if apk == apk_set.primary: + context.manifest_doc["_xml"] = xml_text + context.manifest_infos = infos + primary = infos[0] + if primary.parse_error: + raise ToCodeError( + f"cannot parse AndroidManifest.xml of {apk_set.primary.name}: " + f"{primary.parse_error}" + ) + context.package = primary.package or clean_path_component(path.stem) + + +def _bundle_tmp_root() -> str | None: + # Bundles are unpacked here before the package name (and so the export root) + # is known. Honour the same override as the worker database copies so the + # unpacked APKs can be kept off a RAM-backed /tmp. + explicit = os.environ.get("TOCODE_WORKER_TMP_DIR", "").strip() + if not explicit: + return None + candidate = Path(explicit).expanduser() + try: + candidate.mkdir(parents=True, exist_ok=True) + except OSError: + return None + return str(candidate) + + +def _root_dir(context: ApkContext) -> Path: + out_dir = context.options.out_dir + source = context.input_path + if out_dir is not None: + out = Path(out_dir).expanduser() + return out.resolve() if out.is_absolute() else (source.parent / out).resolve() + name = f"{clean_path_component(context.package)}_decompiler" + default_root = os.environ.get("TOCODE_DEFAULT_OUT_ROOT", "").strip() + if default_root: + return (Path(default_root).expanduser().resolve() / name).resolve() + return (source.parent / name).resolve() + + +def _prepare_root(context: ApkContext) -> None: + root = _root_dir(context) + context.root = root + context.raw_dir = root / "src" / "raw" + context.data_dir = root / "data" + if context.progress.log_path is None: + context.progress.set_log_path(root / "tocode.log") + for path in (context.raw_dir, context.data_dir, root / "lib"): + path.mkdir(parents=True, exist_ok=True) + apk_set = _need(context.apk_set) + context.progress.log("Export run started") + context.progress.log(f"Using ASC as backend for {context.package}") + context.progress.log("APK set: " + ", ".join(item.name for item in apk_set.apks)) + xml_text = context.manifest_doc.pop("_xml", "") + manifest_path = root / "AndroidManifest.xml" + write_text_atomic( + manifest_path, xml_text if xml_text.endswith("\n") else xml_text + "\n" + ) + context.manifest_xml_path = manifest_path + context.manifest_doc = meta.manifest_json( + context.manifest_infos[0], context.manifest_infos[1:], xml_path=manifest_path + ) + write_json(root / "manifest.json", context.manifest_doc) + + +def _extract_entries(context: ApkContext) -> None: + root = _need(context.root) + apk_set = _need(context.apk_set) + data_root = _need(context.data_dir) / "apk" + entries: list[meta.ApkEntry] = [] + total = 0 + for apk in apk_set.apks: + with zipfile.ZipFile(apk) as archive: + total += len(archive.infolist()) + with context.progress.bar(total=total, desc="extract", unit="entry") as bar: + for apk in apk_set.apks: + target_root = data_root / clean_path_component(apk.stem) + with zipfile.ZipFile(apk) as archive: + for info in archive.infolist(): + bar.update(1) + if info.is_dir(): + continue + name = info.filename + with archive.open(info) as handle: + head = handle.read(4) + kind = _entry_kind(name, head) + exported: str | None = None + if kind not in {"dex", "native"}: + target = _safe_join(target_root, name) + if target is not None: + target.parent.mkdir(parents=True, exist_ok=True) + with ( + archive.open(info) as source, + target.open("wb") as sink, + ): + shutil.copyfileobj(source, sink, 1 << 20) + exported = target.relative_to(root).as_posix() + entries.append( + meta.ApkEntry( + apk=apk.name, + name=name, + size=info.file_size, + compressed_size=info.compress_size, + kind=kind, + exported_path=exported, + ) + ) + context.entries = entries + context.libs = extract_native_libs(apk_set.apks, root, sha256=sha256_file) + for lib in context.libs: + lib.package = context.package + for entry in entries: + if entry.name == lib.entry and entry.apk == lib.source_apk: + entry.exported_path = lib.path.relative_to(root).as_posix() + context.progress.log( + f"Extracted {len(entries)} entries, {len(context.libs)} native libraries" + ) + _write_native_libs(context) + + +def _entry_kind(name: str, head: bytes) -> str: + leaf = name.rsplit("/", 1)[-1] + if "/" not in name and DEX_ENTRY_RX.match(leaf): + return "dex" + if classify_native_entry(name, head) is not None: + return "native" + if name == "AndroidManifest.xml": + return "manifest" + if name == "resources.arsc": + return "arsc" + if name.startswith("res/"): + return "resource" + if name.startswith("assets/"): + return "asset" + if name.startswith("META-INF/"): + return "meta" + if name.startswith("kotlin/"): + return "kotlin" + return "other" + + +def _safe_join(root: Path, name: str) -> Path | None: + parts = [part for part in name.split("/") if part not in {"", ".", ".."}] + if not parts: + return None + return root.joinpath(*parts) + + +def _start_native(context: ApkContext, exporter: NativeExporter | None) -> None: + root = _need(context.root) + if not context.libs: + return + if not context.options.native: + for lib in context.libs: + lib.status = "skipped: --no-native" + context.progress.log( + f"Native decompilation skipped (--no-native) for {len(context.libs)} libraries" + ) + _write_native_libs(context) + return + runner = NativeRunner( + libs=context.libs, + root=root, + progress=context.progress, + exporter=exporter or default_native_exporter(context.options.native_options), + min_free_mb=_native_min_free_mb(), + available_memory=available_memory_mb, + ) + context.progress.log( + f"Native decompilation started in background for {len(context.libs)} " + f"libraries (backend: {context.options.native_options.backend})" + ) + runner.start() + context.native_runner = runner + + +def _native_min_free_mb() -> int: + raw = os.environ.get("TOCODE_APK_NATIVE_MIN_FREE_MB", "").strip() + if raw: + try: + return max(0, int(raw)) + except ValueError: + pass + return DEFAULT_NATIVE_MIN_FREE_MB + + +def _join_native(context: ApkContext) -> None: + runner = context.native_runner + if runner is None: + return + thread = runner.thread + if thread is not None and thread.is_alive(): + pending = sum(1 for lib in context.libs if lib.status == "pending") + context.progress.log( + f"DEX export done; waiting for {pending} native library export(s) " + "(progress below is mirrored from native///tocode.log)" + ) + # Heartbeat while the native side is in a silent phase (loading or + # auto-analysis produce no log lines for a while). + while thread.is_alive(): + thread.join(timeout=NATIVE_HEARTBEAT_SECONDS) + if thread.is_alive(): + status = runner.describe_current() + if status: + context.progress.log(status) + runner.join() + done = sum(1 for lib in context.libs if lib.status == "done") + context.progress.log( + f"Native decompilation finished: {done}/{len(context.libs)} libraries exported" + ) + _write_native_libs(context) + + +def _write_native_libs(context: ApkContext) -> None: + root = _need(context.root) + write_json(root / "native-libs.json", native_libs_json(context.libs, root)) + + +def _start_resources(context: ApkContext) -> None: + if not context.options.decode_resources: + return + thread = threading.Thread( + target=_decode_resources, args=(context,), name="tocode-resources", daemon=True + ) + thread.start() + context.resource_thread = thread + + +def _join_resources(context: ApkContext) -> None: + thread = context.resource_thread + if thread is None: + return + thread.join() + context.progress.log( + f"Decoded {len(context.resource_files)} resource files" + + ( + f" ({len(context.resource_errors)} failed)" + if context.resource_errors + else "" + ) + ) + for line in context.resource_errors[:20]: + context.progress.file_log(f"resource decode failed: {line}") + + +def _decode_resources(context: ApkContext) -> None: + root = _need(context.root) + data_dir = _need(context.data_dir) + try: + from androguard.core import axml as androguard_axml + except Exception as exc: # pragma: no cover - androguard ships with droidasc + context.resource_errors.append(f"androguard unavailable: {exc}") + return + resources: dict[str, Any] = {"packages": []} + for entry in context.entries: + if entry.exported_path is None: + continue + source = root / entry.exported_path + if entry.kind == "resource" and entry.name.endswith(".xml"): + target = data_dir / "res" / clean_path_component(Path(entry.apk).stem) + target = target.joinpath(*entry.name.split("/")[1:]) + try: + data = source.read_bytes() + if not data.startswith(b"\x03\x00\x08\x00"): + continue # already plain text + printer = androguard_axml.AXMLPrinter(data) + if not printer.is_valid(): + raise ValueError("invalid binary XML") + text = printer.get_xml(pretty=True).decode("utf-8", errors="replace") + write_text_atomic(target, text) + context.resource_files.append(target) + except Exception as exc: + context.resource_errors.append(f"{entry.apk}:{entry.name}: {exc}") + elif entry.kind == "arsc": + try: + resources["packages"].extend( + _decode_arsc(androguard_axml, source.read_bytes(), entry.apk) + ) + except Exception as exc: + context.resource_errors.append(f"{entry.apk}:{entry.name}: {exc}") + write_json(data_dir / "resources.json", resources) + + +def _decode_arsc(axml_module: Any, data: bytes, apk_name: str) -> list[dict[str, Any]]: + parser = axml_module.ARSCParser(data) + packages: list[dict[str, Any]] = [] + resolved = parser.get_resolved_strings() + for package in parser.get_packages_names(): + strings: dict[str, dict[str, str]] = {} + for locale, table in (resolved.get(package) or {}).items(): + strings[str(locale) or "default"] = { + f"0x{res_id:08x}" if isinstance(res_id, int) else str(res_id): value + for res_id, value in table.items() + } + public: list[dict[str, Any]] = [] + try: + public_xml = parser.get_public_resources(package).decode( + "utf-8", errors="replace" + ) + for match in re.finditer( + r' None: + apk_set = _need(context.apk_set) + session = AscSession(apk_set) + with context.progress.bar(total=1, desc="inventory", unit="step") as bar: + session.load() + bar.update(1) + context.session = session + context.progress.log( + f"Inventory: {len(session.images)} dex, {len(session.classes)} classes, " + f"{len(session.methods)} methods, {len(session.strings)} strings" + ) + _assign_java_paths(context) + meta.entry_methods(session, context.manifest_doc) + + +def _assign_java_paths(context: ApkContext) -> None: + raw_dir = _need(context.raw_dir) + session = _need(context.session) + seen: dict[str, int] = {} + for record in session.classes: + rel = class_java_relpath(record.descriptor) + key = rel.as_posix().lower() + count = seen.get(key, 0) + seen[key] = count + 1 + if count: + rel = rel.with_name(f"{rel.stem}~{count}{rel.suffix}") + record.java_path = raw_dir / rel + + +def _select_workers(context: ApkContext) -> int: + session = _need(context.session) + chosen = choose_jobs( + function_count=len(session.classes), + analysis_seconds=0.0, + requested=context.options.jobs, + backend="asc", + ) + memory = available_memory_mb() + if memory is not None and chosen > 1: + ceiling = max(1, memory // ASC_WORKER_MEMORY_MB) + if ceiling < chosen: + context.progress.log( + f"Limiting decompile workers to {ceiling} " + f"({memory} MB available, {ASC_WORKER_MEMORY_MB} MB per worker)" + ) + chosen = ceiling + context.worker_count = max(1, chosen) + context.render_mode = "process" if context.worker_count > 1 else "single" + return context.worker_count + + +def _decompile( + context: ApkContext, decompiler_factory: Callable[[AscSession], Any] | None +) -> None: + session = _need(context.session) + classes = session.classes + if not classes: + context.progress.log("No DEX classes to decompile") + _remove_stale_sources(context, set()) + return + workers = _select_workers(context) + expected = {record.java_path for record in classes if record.java_path} + _remove_stale_sources(context, {path for path in expected if path}) + context.progress.log(f"Decompiling {len(classes)} classes with {workers} worker(s)") + with context.progress.bar( + total=len(classes), desc="decompile", unit="class" + ) as bar: + if decompiler_factory is not None or workers <= 1: + _decompile_serial(context, decompiler_factory, bar) + else: + try: + _decompile_parallel(context, workers, bar) + except _PoolFailure as exc: + context.progress.log( + f"Worker pool failed ({exc}); finishing remaining classes in-process" + ) + context.render_mode = "single-fallback" + _decompile_serial(context, None, bar) + context.progress.log( + f"Decompiled {len(classes) - len(context.failures)} classes, " + f"{len(context.failures)} failed" + ) + + +class _PoolFailure(RuntimeError): + pass + + +def _pending_classes(context: ApkContext) -> list[ClassRecord]: + session = _need(context.session) + return [ + record + for record in session.classes + if record.line_count == 0 and record.error is None + ] + + +def _decompile_serial( + context: ApkContext, factory: Callable[[AscSession], Any] | None, bar: Any +) -> None: + session = _need(context.session) + decompiler = factory(session) if factory is not None else InlineDecompiler(session) + for record in _pending_classes(context): + result = decompiler.decompile(record) + _store_result(context, record, result) + bar.update(1) + + +def _decompile_parallel(context: ApkContext, workers: int, bar: Any) -> None: + pending = _pending_classes(context) + by_descriptor = {record.descriptor: record for record in pending} + # Group by dex so each worker keeps its DexManager hot for long stretches. + ordered = sorted(pending, key=lambda record: (record.dex, record.index)) + batches: list[list[tuple[str, str, int]]] = [] + for start in range(0, len(ordered), DECOMPILE_BATCH_SIZE): + chunk = ordered[start : start + DECOMPILE_BATCH_SIZE] + batches.append([(item.dex, item.descriptor, item.index) for item in chunk]) + context.progress.file_log( + f"decompile: {len(batches)} batches of up to {DECOMPILE_BATCH_SIZE} classes" + ) + # Workers are recycled between rounds to keep their resident size bounded. + # (``max_tasks_per_child`` is not used: it deadlocks spawn pools on some + # CPython builds, e.g. 3.14.4, after the first recycle.) + round_size = workers * ASC_WORKER_BATCHES_PER_CHILD + for start in range(0, len(batches), round_size): + _run_decompile_round( + context, workers, batches[start : start + round_size], by_descriptor, bar + ) + + +def _run_decompile_round( + context: ApkContext, + workers: int, + batches: list[list[tuple[str, str, int]]], + by_descriptor: dict[str, ClassRecord], + bar: Any, +) -> None: + apk_set = _need(context.apk_set) + executor = ProcessPoolExecutor( + max_workers=min(workers, len(batches)), + mp_context=multiprocessing.get_context("spawn"), + initializer=init_decompile_worker, + initargs=(apk_set,), + ) + try: + futures: dict[Future[list[DecompiledClass]], list[tuple[str, str, int]]] = {} + for batch in batches: + futures[executor.submit(decompile_batch_in_worker, batch)] = batch + for future in as_completed(futures): + batch = futures[future] + try: + results = future.result() + except Exception as exc: + raise _PoolFailure(f"{type(exc).__name__}: {exc}") from exc + for result in results: + record = by_descriptor.get(result.descriptor) + if record is not None: + _store_result(context, record, result) + bar.update(len(batch)) + finally: + executor.shutdown(wait=True, cancel_futures=True) + + +def _store_result( + context: ApkContext, record: ClassRecord, result: DecompiledClass +) -> None: + session = _need(context.session) + root = _need(context.root) + session.apply_refs(record, result.refs) + path = _need(record.java_path) + if result.source is None: + record.error = result.error or "unknown decompilation error" + context.failures.append((record.descriptor, record.error)) + text = _failure_stub(record) + context.progress.file_log(f"export {record.descriptor} failed: {record.error}") + else: + text = result.source if result.source.endswith("\n") else result.source + "\n" + text = _annotate_source(record, text) + write_text_atomic(path, text) + record.line_count = text.count("\n") + assign_method_lines(record, text) + context.source_files.append(path) + if record.error is None: + context.progress.file_log( + f"export {record.descriptor} - {record.line_count} lines done " + f"({path.relative_to(root).as_posix()})" + ) + + +def _annotate_source(record: ClassRecord, text: str) -> str: + header = ( + f"// ToCode: {record.descriptor} from {record.dex}" + + (f" (source: {record.source_file})" if record.source_file else "") + + "\n" + ) + return header + text + + +def _failure_stub(record: ClassRecord) -> str: + lines = [ + f"// ToCode: {record.descriptor} from {record.dex}", + f"// decompilation failed: {record.error}", + f"package {record.package};" if record.package else "", + f"{' '.join(record.flag_names)} class {record.simple_name} {{", + ] + for method in record.methods: + lines.append( + f" {' '.join(method.flag_names)} {method.prototype};".replace(" ", " ") + ) + lines.append("}") + return "\n".join(line for line in lines if line is not None) + "\n" + + +def assign_method_lines(record: ClassRecord, text: str) -> None: + """Best-effort mapping of each method to its line range in the Java text. + + DAD prints constructors and the static initializer under the class's + simple name (``Outer$Inner(...)`` / ``static Outer$Inner()``), so those are + keyed by that name and told apart by the ``static`` modifier. + """ + lines = text.splitlines() + simple = record.simple_name + by_name: dict[str, list[MethodRecord]] = {} + for method in record.methods: + method.line_start = None + method.line_end = None + name = simple if method.name in {"", ""} else method.name + by_name.setdefault(name, []).append(method) + starts: list[tuple[int, str, int, bool, bool]] = [] + for number, line in enumerate(lines, start=1): + match = METHOD_DECL_RX.match(line) + if not match: + continue + name = match.group("name") + if name not in by_name: + continue + count = _param_count(match.group("params").strip()) + has_body = match.group("end") != ";" + is_static = " static " in f" {match.group('decl')[: match.start('name') - 4]} " + starts.append((number, name, count, has_body, is_static)) + total = len(lines) + for position, (start, name, count, has_body, is_static) in enumerate(starts): + candidates = [m for m in by_name.get(name, []) if m.line_start is None] + if name == simple: + wanted = "" if is_static else "" + special = [m for m in candidates if m.name == wanted] + candidates = special or [ + m for m in candidates if m.name not in {"", ""} + ] + if not candidates: + continue + chosen = next((m for m in candidates if len(m.params) == count), candidates[0]) + # Annotations directly above the declaration belong to the method. + first = start + while first > 1 and lines[first - 2].startswith(" @"): + first -= 1 + if not has_body: + end = start + else: + closing = _closing_brace(lines, start) + if closing is None: + closing = ( + starts[position + 1][0] - 1 if position + 1 < len(starts) else total + ) + end = closing + chosen.line_start = first + chosen.line_end = end + if record.error is not None: + for method in record.methods: + if method.line_start is None: + method.line_start = 1 + method.line_end = max(total, 1) + + +def _param_count(params: str) -> int: + if not params: + return 0 + depth = 0 + count = 1 + for ch in params: + if ch in "<([": + depth += 1 + elif ch in ">)]": + depth -= 1 + elif ch == "," and depth == 0: + count += 1 + return count + + +def _closing_brace(lines: list[str], start: int) -> int | None: + for number in range(start, len(lines) + 1): + if lines[number - 1] == " }": + return number + return None + + +def _remove_stale_sources(context: ApkContext, expected: set[Path]) -> None: + raw_dir = _need(context.raw_dir) + if not raw_dir.exists(): + return + removed = 0 + for path in raw_dir.rglob("*.java"): + if path not in expected: + path.unlink(missing_ok=True) + removed += 1 + if removed: + context.progress.file_log(f"removed {removed} stale source files") + + +def _write_metadata(context: ApkContext) -> None: + root = _need(context.root) + session = _need(context.session) + with context.progress.bar(total=9, desc="metadata", unit="doc") as bar: + # The class/method/string documents are written row by row: on a large + # APK they reach hundreds of megabytes, which cannot be built as one + # Python structure plus its serialized copy in memory. + meta.write_json_rows( + root / "classes.json", + "classes", + meta.iter_class_rows(session, root), + header={"count": len(session.classes)}, + ) + bar.update(1) + meta.write_json_rows( + root / "functions.json", + "functions", + meta.iter_function_rows(session, root), + ) + bar.update(1) + meta.write_json_rows( + root / "function-index.json", + "functions", + meta.iter_function_index_rows(session, root), + header={"schema_version": 2, "language": "java"}, + ) + bar.update(1) + meta.write_json_rows( + root / "strings.json", "strings", meta.iter_string_rows(session) + ) + bar.update(1) + write_json(root / "imports.json", meta.imports_json(session)) + bar.update(1) + write_json( + root / "exports.json", meta.exports_json(session, context.manifest_doc) + ) + bar.update(1) + write_json(root / "sections.json", meta.sections_json(context.entries)) + bar.update(1) + write_json(root / "package-graph.json", meta.package_graph_json(session)) + bar.update(1) + seeds = meta.entry_methods(session, context.manifest_doc) + context.reachable = meta.reachable_json(session, seeds) + write_json(root / "reachable.json", context.reachable) + bar.update(1) + + +def _write_final_documents(context: ApkContext) -> None: + root = _need(context.root) + session = _need(context.session) + reachable = context.reachable + libs_doc = native_libs_json(context.libs, root) + write_json( + root / "triage.json", + meta.triage_json( + session, context.manifest_doc, reachable, libs_doc["libs"], context.entries + ), + ) + apk_set = _need(context.apk_set) + native_exports = [ + { + "abi": lib.abi, + "name": lib.name, + "export_dir": str(lib.export_dir) if lib.export_dir else None, + "status": lib.status, + "backend": lib.backend, + } + for lib in context.libs + ] + common: dict[str, Any] = { + "binary": str(context.input_path), + "apks": [str(item) for item in apk_set.apks], + "package": context.package, + "backend": "asc", + "decompiler": AscSession.decompiler_label, + "root_dir": str(root), + "src_dir": str(_need(context.raw_dir)), + "raw_src_dir": str(_need(context.raw_dir)), + "tree_src_dir": None, + "include_dir": None, + "data_dir": str(_need(context.data_dir)), + "header": None, + "agents": str(root / "AGENTS.md"), + "claude": str(root / "CLAUDE.md"), + "log": str(root / "tocode.log"), + "manifest_xml": str(context.manifest_xml_path) + if context.manifest_xml_path + else None, + "manifest": str(root / "manifest.json"), + "ida_database": None, + "source_files": [str(item) for item in context.source_files], + "tree_source_files": [], + "summary_files": [], + "asm_files": [], + "function_index": str(root / "function-index.json"), + "tree_function_index": None, + "function_count": len(session.methods), + "class_count": len(session.classes), + "cluster_count": 0, + "failure_count": len(context.failures), + "requested_worker_count": context.options.jobs + if context.options.jobs is not None + else "auto", + "worker_count": context.worker_count, + "parallel_mode": context.render_mode, + "native_enabled": context.options.native, + "native_exports": native_exports, + } + write_json(root / "project.json", common) + manifest: dict[str, Any] = dict(common) + manifest.update( + { + "schema_version": 2, + "tocode_version": __version__, + "format": "apk", + "arch": "dalvik", + "bits": 32, + "entrypoints": context.manifest_doc.get("main_activities", []), + "raw_source_files": [str(item) for item in context.source_files], + "data_variable_count": 0, + "triage": str(root / "triage.json"), + "imports": str(root / "imports.json"), + "exports": str(root / "exports.json"), + "reachable": str(root / "reachable.json"), + "cluster_graph": None, + "package_graph": str(root / "package-graph.json"), + "classes": str(root / "classes.json"), + "strings": str(root / "strings.json"), + "sections": str(root / "sections.json"), + "native_libs": str(root / "native-libs.json"), + "resources": str(_need(context.data_dir) / "resources.json"), + "resource_files": len(context.resource_files), + "types": None, + "type_count": 0, + "variables_interesting": None, + "dex_files": [image.name for image in session.images], + "failures": [ + {"address": None, "name": descriptor, "error": error} + for descriptor, error in context.failures + ], + "native_errors": context.native_runner.errors + if context.native_runner + else [], + } + ) + write_json(root / "export-manifest.json", manifest) + write_text_atomic( + root / "AGENTS.md", + meta.build_apk_agents( + package=context.package, + native=context.options.native and bool(context.libs), + native_count=len(context.libs), + ) + + "\n", + ) + write_text_atomic(root / "CLAUDE.md", "@./AGENTS.md\n") + context.progress.log("Wrote project.json, export-manifest.json, AGENTS.md") + + +def _summary(context: ApkContext) -> ApkExportSummary: + session = context.session + apk_set = _need(context.apk_set) + return ApkExportSummary( + root_dir=_need(context.root), + package=context.package, + apks=list(apk_set.apks), + class_count=len(session.classes) if session else 0, + method_count=len(session.methods) if session else 0, + source_files=list(context.source_files), + failed_classes=list(context.failures), + native_total=len(context.libs), + native_done=sum(1 for lib in context.libs if lib.status == "done"), + native_errors=list(context.native_runner.errors) + if context.native_runner + else [], + manifest_path=context.manifest_xml_path, + seconds=time.monotonic() - context.started, + ) + + +def _need(value: Any) -> Any: + if value is None: + raise RuntimeError("export context not initialized") + return value + + +def iter_source_files(root: Path) -> Iterable[Path]: + yield from sorted((root / "src" / "raw").rglob("*.java")) diff --git a/src/tocode/apk_metadata.py b/src/tocode/apk_metadata.py new file mode 100644 index 0000000..753ca10 --- /dev/null +++ b/src/tocode/apk_metadata.py @@ -0,0 +1,1012 @@ +"""JSON metadata documents for APK exports. + +The document shapes mirror ``metadata.py`` wherever a native concept has a +Dalvik equivalent (functions, strings, imports, exports, sections, reachable, +triage, project, export-manifest) so agents and tooling that already read +ToCode exports keep working. Android-only facts live in ``manifest.json``, +``classes.json``, ``package-graph.json``, and ``native-libs.json``. +""" + +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass, field +import json +from pathlib import Path +import re +import tempfile +from typing import Any, Iterable, Iterator +import xml.etree.ElementTree as ET + +from .backends.asc import ( + AscSession, + ClassRecord, + MethodRecord, + StringRecord, + java_type, +) +from .metadata import _replace_with_retry + + +ANDROID_NS = "http://schemas.android.com/apk/res/android" +_A = f"{{{ANDROID_NS}}}" + +COMPONENT_TAGS = ("activity", "activity-alias", "service", "receiver", "provider") +DANGEROUS_PERMISSION_HINTS = ( + "READ_SMS", + "SEND_SMS", + "RECEIVE_SMS", + "READ_CONTACTS", + "WRITE_CONTACTS", + "READ_CALL_LOG", + "WRITE_CALL_LOG", + "CALL_PHONE", + "RECORD_AUDIO", + "CAMERA", + "ACCESS_FINE_LOCATION", + "ACCESS_COARSE_LOCATION", + "ACCESS_BACKGROUND_LOCATION", + "READ_EXTERNAL_STORAGE", + "WRITE_EXTERNAL_STORAGE", + "MANAGE_EXTERNAL_STORAGE", + "REQUEST_INSTALL_PACKAGES", + "SYSTEM_ALERT_WINDOW", + "BIND_ACCESSIBILITY_SERVICE", + "BIND_DEVICE_ADMIN", + "READ_PHONE_STATE", + "GET_ACCOUNTS", + "BLUETOOTH_CONNECT", + "QUERY_ALL_PACKAGES", + "RECEIVE_BOOT_COMPLETED", + "PACKAGE_USAGE_STATS", + "BIND_NOTIFICATION_LISTENER_SERVICE", +) +# Strings worth reading first, in priority order (URLs/endpoints and secrets +# before generic crypto and platform hints). Each entry is (tier, pattern). +INTERESTING_STRING_TIERS: tuple[tuple[int, re.Pattern[str]], ...] = ( + ( + 0, + re.compile( + r"(https?://|wss?://|ftp://|content://|\.onion\b|" + r"BEGIN (RSA|EC|OPENSSH|PGP)? ?PRIVATE KEY|" + r"\b(api[_-]?key|secret[_-]?key|client[_-]?secret|access[_-]?token|" + r"bearer|authorization|passw(or)?d)\b)", + re.IGNORECASE, + ), + ), + ( + 1, + re.compile( + r"(/data/(local|data)/|/system/(bin|xbin|app)/|/proc/self|/dev/socket|" + r"/bin/sh\b|\bsu\b|superuser|magisk|\bxposed|\bfrida|substrate|" + r"\b(goldfish|ranchu|genymotion|bluestacks|qemu)\b|" + r"System\.loadLibrary|dlopen|Runtime\.exec|ptrace|" + r"\b\d{1,3}(\.\d{1,3}){3}(:\d{2,5})?\b)", + re.IGNORECASE, + ), + ), + ( + 2, + re.compile( + r"^(javax\.crypto|AES/|RSA/|DES/|DESede/|HmacSHA|PBKDF2|SHA-?256|" + r"SecretKeySpec|network_security_config|DisabledHostnameVerifier)", + ), + ), +) + + +def write_json_rows( + path: Path, + key: str, + rows: Iterable[dict[str, Any]], + *, + header: dict[str, Any] | None = None, +) -> int: + """Write ``{**header, key: [...rows]}`` without holding the array in memory. + + The big APK documents (one row per method, class, or string) reach hundreds + of megabytes; ``json.dumps`` on the whole structure would need several + copies of that at once, so rows are serialized one at a time straight into + the file and the result is renamed into place like ``write_json``. + """ + path.parent.mkdir(parents=True, exist_ok=True) + count = 0 + with tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as handle: + tmp_path = Path(handle.name) + handle.write("{\n") + for name, value in (header or {}).items(): + handle.write(f" {json.dumps(name)}: {json.dumps(value)},\n") + handle.write(f" {json.dumps(key)}: [") + for row in rows: + handle.write("\n " if count == 0 else ",\n ") + handle.write(json.dumps(row)) + count += 1 + handle.write("\n ]\n" if count else "]\n") + handle.write("}\n") + try: + _replace_with_retry(tmp_path, path) + except OSError: + tmp_path.unlink(missing_ok=True) + raise + return count + + +@dataclass(slots=True) +class ManifestComponent: + kind: str + name: str + exported: bool | None + permission: str | None + intent_filters: list[dict[str, list[str]]] + attributes: dict[str, str] + + +@dataclass(slots=True) +class ManifestInfo: + apk: str + package: str + version_code: str | None + version_name: str | None + min_sdk: str | None + target_sdk: str | None + compile_sdk: str | None + split: str | None + permissions: list[str] + defined_permissions: list[str] + features: list[dict[str, Any]] + libraries: list[str] + application: dict[str, Any] + components: list[ManifestComponent] + main_activities: list[str] + parse_error: str | None = None + raw_attributes: dict[str, str] = field(default_factory=dict) + + +def _attr(node: ET.Element, name: str) -> str | None: + value = node.get(f"{_A}{name}") + if value is None: + value = node.get(name) + return value + + +def _bool_attr(node: ET.Element, name: str) -> bool | None: + value = _attr(node, name) + if value is None: + return None + return value.strip().lower() == "true" + + +def _plain_attrs(node: ET.Element) -> dict[str, str]: + result: dict[str, str] = {} + for key, value in node.attrib.items(): + result[key.replace(_A, "android:")] = value + return result + + +def parse_manifest(xml_text: str, *, apk_name: str) -> ManifestInfo: + try: + root = ET.fromstring(xml_text) + except ET.ParseError as exc: + return ManifestInfo( + apk=apk_name, + package="", + version_code=None, + version_name=None, + min_sdk=None, + target_sdk=None, + compile_sdk=None, + split=None, + permissions=[], + defined_permissions=[], + features=[], + libraries=[], + application={}, + components=[], + main_activities=[], + parse_error=str(exc), + ) + uses_sdk = root.find("uses-sdk") + application = root.find("application") + components: list[ManifestComponent] = [] + main_activities: list[str] = [] + app_attrs: dict[str, Any] = {} + if application is not None: + app_attrs = _plain_attrs(application) + for tag in COMPONENT_TAGS: + for node in application.iter(tag): + component = _component(tag, node) + components.append(component) + if tag in {"activity", "activity-alias"} and _is_launcher(component): + main_activities.append(component.name) + return ManifestInfo( + apk=apk_name, + package=root.get("package", "") or "", + version_code=_attr(root, "versionCode"), + version_name=_attr(root, "versionName"), + min_sdk=_attr(uses_sdk, "minSdkVersion") if uses_sdk is not None else None, + target_sdk=_attr(uses_sdk, "targetSdkVersion") + if uses_sdk is not None + else None, + compile_sdk=_attr(root, "compileSdkVersion"), + split=root.get("split"), + permissions=[ + value + for value in (_attr(node, "name") for node in root.iter("uses-permission")) + if value + ] + + [ + value + for value in ( + _attr(node, "name") for node in root.iter("uses-permission-sdk-23") + ) + if value + ], + defined_permissions=[ + value + for value in (_attr(node, "name") for node in root.iter("permission")) + if value + ], + features=[ + { + "name": _attr(node, "name"), + "required": _bool_attr(node, "required"), + "glEsVersion": _attr(node, "glEsVersion"), + } + for node in root.iter("uses-feature") + ], + libraries=[ + value + for value in ( + _attr(node, "name") + for node in list(root.iter("uses-library")) + + list(root.iter("uses-native-library")) + ) + if value + ], + application=app_attrs, + components=components, + main_activities=main_activities, + raw_attributes={ + key.replace(_A, "android:"): value for key, value in root.attrib.items() + }, + ) + + +def _component(tag: str, node: ET.Element) -> ManifestComponent: + filters: list[dict[str, list[str]]] = [] + for item in node.findall("intent-filter"): + filters.append( + { + "actions": [ + value + for value in ( + _attr(child, "name") for child in item.findall("action") + ) + if value + ], + "categories": [ + value + for value in ( + _attr(child, "name") for child in item.findall("category") + ) + if value + ], + "data": [ + " ".join( + f"{key}={value}" + for key, value in sorted(_plain_attrs(child).items()) + ) + for child in item.findall("data") + ], + } + ) + exported = _bool_attr(node, "exported") + if exported is None and filters: + # Pre-API 31 default: components with intent filters are exported. + exported = True + name = _attr(node, "name") or "" + if tag == "activity-alias": + name = _attr(node, "targetActivity") or name + return ManifestComponent( + kind=tag, + name=name, + exported=exported, + permission=_attr(node, "permission"), + intent_filters=filters, + attributes=_plain_attrs(node), + ) + + +def _is_launcher(component: ManifestComponent) -> bool: + for item in component.intent_filters: + if "android.intent.action.MAIN" in item["actions"] and ( + "android.intent.category.LAUNCHER" in item["categories"] + or "android.intent.category.LEANBACK_LAUNCHER" in item["categories"] + ): + return True + return False + + +def resolve_component_class(package: str, name: str) -> str: + if not name: + return name + if name.startswith("."): + return f"{package}{name}" + if "." not in name and package: + return f"{package}.{name}" + return name + + +def class_descriptor(java_name: str) -> str: + return f"L{java_name.replace('.', '/')};" + + +def _component_json(package: str, component: ManifestComponent) -> dict[str, Any]: + resolved = resolve_component_class(package, component.name) + return { + "kind": component.kind, + "name": resolved, + "descriptor": class_descriptor(resolved) if resolved else None, + "exported": component.exported, + "permission": component.permission, + "intent_filters": component.intent_filters, + "attributes": component.attributes, + } + + +def manifest_json( + base: ManifestInfo, splits: list[ManifestInfo], *, xml_path: Path +) -> dict[str, Any]: + package = base.package + return { + "package": package, + "apk": base.apk, + "manifest_xml": str(xml_path), + "version_code": base.version_code, + "version_name": base.version_name, + "min_sdk": base.min_sdk, + "target_sdk": base.target_sdk, + "compile_sdk": base.compile_sdk, + "attributes": base.raw_attributes, + "application": base.application, + "main_activities": [ + resolve_component_class(package, item) for item in base.main_activities + ], + "permissions": base.permissions, + "dangerous_permissions": dangerous_permissions(base.permissions), + "defined_permissions": base.defined_permissions, + "uses_features": base.features, + "uses_libraries": base.libraries, + "components": [_component_json(package, item) for item in base.components], + "exported_components": [ + _component_json(package, item) for item in base.components if item.exported + ], + "parse_error": base.parse_error, + "splits": [ + { + "apk": item.apk, + "split": item.split, + "package": item.package, + "attributes": item.raw_attributes, + "parse_error": item.parse_error, + } + for item in splits + ], + } + + +def dangerous_permissions(permissions: Iterable[str]) -> list[str]: + return [ + item + for item in permissions + if any(item.endswith(hint) for hint in DANGEROUS_PERMISSION_HINTS) + ] + + +# -------------------------------------------------------------------------- +# Classes / functions +# -------------------------------------------------------------------------- + + +def _relpath(path: Path | None, root: Path) -> str | None: + if path is None: + return None + try: + return path.relative_to(root).as_posix() + except ValueError: + return str(path) + + +def _method_json( + method: MethodRecord, session: AscSession, root: Path +) -> dict[str, Any]: + owner = session.class_by_descriptor(method.class_descriptor) + return { + "address": method.address, + "dex": method.dex, + "method_index": method.index, + "name": method.name, + "class": java_type(method.class_descriptor), + "descriptor": method.descriptor, + "prototype": method.prototype, + "access": method.flag_names, + "native": method.is_native, + "abstract": method.is_abstract, + "static": method.is_static, + "code_size": method.code_size, + "registers": method.registers, + "source_file": _relpath(owner.java_path, root) if owner else None, + "source_line_start": method.line_start, + "source_line_end": method.line_end, + } + + +def iter_class_rows(session: AscSession, root: Path) -> Iterator[dict[str, Any]]: + for record in session.classes: + yield { + "descriptor": record.descriptor, + "name": record.java_name, + "package": record.package, + "simple_name": record.simple_name, + "dex": record.dex, + "class_index": record.index, + "access": record.flag_names, + "interface": record.is_interface, + "superclass": java_type(record.superclass) if record.superclass else None, + "interfaces": [java_type(item) for item in record.interfaces], + "source_file": record.source_file, + "java_file": _relpath(record.java_path, root), + "line_count": record.line_count, + "decompile_error": record.error, + "method_count": len(record.methods), + "native_method_count": sum(1 for m in record.methods if m.is_native), + "methods": [ + { + "name": item.name, + "address": item.address, + "prototype": item.prototype, + "access": item.flag_names, + "native": item.is_native, + "code_size": item.code_size, + "line_start": item.line_start, + "line_end": item.line_end, + } + for item in record.methods + ], + "fields": [ + { + "name": item.name, + "type": java_type(item.type), + "descriptor": item.type, + "access": item.flag_names, + "static": item.is_static, + } + for item in record.fields + ], + } + + +def iter_function_rows(session: AscSession, root: Path) -> Iterator[dict[str, Any]]: + by_id = {method.id: method for method in session.methods} + for method in session.methods: + owner = session.class_by_descriptor(method.class_descriptor) + yield { + "address": method.address, + "name": method.descriptor, + "c_name": f"{java_type(method.class_descriptor)}.{method.name}", + "prototype": method.prototype, + "size": method.code_size, + "calltype": "static" if method.is_static else "virtual", + "return_type": java_type(method.return_type), + "params": [ + {"name": f"p{index}", "type": java_type(item)} + for index, item in enumerate(method.params) + ], + "locals": [], + "decl_file": owner.source_file if owner else None, + "decl_dir": None, + "decl_line": None, + "nargs": len(method.params), + "nlocals": max(0, method.registers - len(method.params)), + "stackframe": method.registers, + "callees": [ + by_id[item].address for item in method.callees if item in by_id + ], + "callees_imports": method.external_calls, + "callee_count": len(method.callees) + len(method.external_calls), + "callers": [ + by_id[item].address for item in method.callers if item in by_id + ], + "caller_count": len(method.callers), + "dead_code": not method.callers + and not (owner and _is_entry_class(owner)) + and method.name not in {"", ""}, + "source_file": _relpath(owner.java_path, root) if owner else None, + "source_line_start": method.line_start, + "source_line_end": method.line_end, + "tree_source_file": None, + "tree_source_line_start": None, + "tree_source_line_end": None, + "asm_file": None, + "asm_line_start": None, + "asm_line_end": None, + "dex": method.dex, + "class": java_type(method.class_descriptor), + "access": method.flag_names, + "native": method.is_native, + "string_ref_count": method.string_ref_count, + "field_refs": method.field_refs, + } + + +def _is_entry_class(record: ClassRecord) -> bool: + return record.entry + + +def iter_function_index_rows( + session: AscSession, root: Path +) -> Iterator[dict[str, Any]]: + for method in session.methods: + owner = session.class_by_descriptor(method.class_descriptor) + java_file = _relpath(owner.java_path, root) if owner else None + yield { + "address": method.address, + "name": method.descriptor, + "c": { + "path": java_file, + "line_start": method.line_start, + "line_end": method.line_end, + } + if java_file + else None, + "asm": None, + "origin": {"file": owner.source_file, "line": None} + if owner and owner.source_file + else None, + } + + +# -------------------------------------------------------------------------- +# Strings / imports / exports / sections +# -------------------------------------------------------------------------- + + +def iter_string_rows(session: AscSession) -> Iterator[dict[str, Any]]: + by_id = {method.id: method for method in session.methods} + for item in session.strings: + xrefs = [ + { + "function": by_id[method_id].descriptor, + "address": by_id[method_id].address, + "access": "read", + } + for method_id in session.string_xrefs.get((item.dex, item.index), ()) + if method_id in by_id + ] + yield { + "vaddr": f"{item.dex}:0x{item.offset:x}", + "paddr": f"0x{item.offset:x}", + "size": len(item.value.encode("utf-8", errors="replace")), + "length": len(item.value), + "section": item.dex, + "type": "mutf8", + "value": item.value, + "xrefs": xrefs, + } + + +def imports_json(session: AscSession) -> dict[str, Any]: + groups: dict[str, dict[str, dict[str, Any]]] = {} + for method in session.methods: + for descriptor in method.external_calls: + owner = descriptor.split("->", 1)[0] + package = java_type(owner) + package = package.rsplit(".", 1)[0] if "." in package else package + entry = groups.setdefault(package, {}).setdefault( + descriptor, + { + "name": descriptor, + "address": None, + "delay": False, + "class": java_type(owner), + "callers": [], + }, + ) + if len(entry["callers"]) < 32: + entry["callers"].append(method.address) + imports: list[dict[str, Any]] = [] + for package, members in sorted(groups.items()): + rows: list[dict[str, Any]] = sorted( + members.values(), key=lambda row: str(row["name"]) + ) + imports.append({"dll": package, "functions": rows}) + return {"imports": imports} + + +def exports_json(session: AscSession, manifest: dict[str, Any]) -> dict[str, Any]: + rows: list[dict[str, Any]] = [] + for ordinal, component in enumerate(manifest.get("exported_components", [])): + record = ( + session.class_by_descriptor(component["descriptor"]) + if component.get("descriptor") + else None + ) + rows.append( + { + "ordinal": ordinal, + "name": component["name"], + "address": record.methods[0].address + if record and record.methods + else None, + "is_forwarder": False, + "forwarder_target": None, + "kind": component["kind"], + "descriptor": component["descriptor"], + "defined_in_dex": record is not None, + "intent_filters": component["intent_filters"], + } + ) + for method in session.methods: + if method.is_native: + rows.append( + { + "ordinal": len(rows), + "name": method.descriptor, + "address": method.address, + "is_forwarder": True, + "forwarder_target": jni_symbol(method), + "kind": "jni", + "descriptor": method.descriptor, + "defined_in_dex": True, + "intent_filters": [], + } + ) + return {"exports": rows} + + +def jni_symbol(method: MethodRecord) -> str: + def mangle(text: str) -> str: + out: list[str] = [] + for ch in text: + if ch == "_": + out.append("_1") + elif ch == ";": + out.append("_2") + elif ch == "[": + out.append("_3") + elif ch == "/" or ch == ".": + out.append("_") + elif ch.isascii() and ch.isalnum(): + out.append(ch) + else: + out.append(f"_0{ord(ch):04x}") + return "".join(out) + + owner = method.class_descriptor[1:-1] + return f"Java_{mangle(owner)}_{mangle(method.name)}" + + +@dataclass(slots=True) +class ApkEntry: + apk: str + name: str + size: int + compressed_size: int + kind: str + exported_path: str | None = None + + +def sections_json(entries: list[ApkEntry]) -> dict[str, Any]: + rows = [] + for item in entries: + rows.append( + { + "name": item.name, + "apk": item.apk, + "vaddr": None, + "paddr": None, + "size": item.size, + "vsize": item.compressed_size, + "type": item.kind, + "permissions": "r--", + "rwx": False, + "entropy": None, + "path": item.exported_path, + } + ) + return {"sections": rows} + + +# -------------------------------------------------------------------------- +# Reachability / triage / package graph +# -------------------------------------------------------------------------- + + +def entry_methods(session: AscSession, manifest: dict[str, Any]) -> list[MethodRecord]: + seeds: list[MethodRecord] = [] + seen: set[int] = set() + descriptors = [ + item["descriptor"] + for item in manifest.get("components", []) + if item.get("descriptor") + ] + app_name = manifest.get("application", {}).get("android:name") + if app_name: + descriptors.insert( + 0, class_descriptor(resolve_component_class(manifest["package"], app_name)) + ) + for descriptor in descriptors: + record = session.class_by_descriptor(descriptor) + if record is None: + continue + record.entry = True + for method in record.methods: + if method.id not in seen: + seen.add(method.id) + seeds.append(method) + return seeds + + +def reachable_json(session: AscSession, seeds: list[MethodRecord]) -> dict[str, Any]: + by_id = {method.id: method for method in session.methods} + depths: dict[int, int] = {} + queue: deque[int] = deque() + for seed in seeds: + if seed.id not in depths: + depths[seed.id] = 0 + queue.append(seed.id) + while queue: + current = queue.popleft() + depth = depths[current] + for callee in by_id[current].callees: + if callee not in depths: + depths[callee] = depth + 1 + queue.append(callee) + rows = [ + { + "address": by_id[item].address, + "name": by_id[item].descriptor, + "depth": depth, + } + for item, depth in sorted(depths.items(), key=lambda pair: (pair[1], pair[0])) + ] + return { + "reachable": rows, + "unreachable_count": len(session.methods) - len(depths), + "seed_count": len(seeds), + } + + +def interesting_strings( + strings: Iterable[StringRecord], *, limit: int = 100 +) -> list[dict[str, Any]]: + buckets: dict[int, list[dict[str, Any]]] = { + tier: [] for tier, _ in INTERESTING_STRING_TIERS + } + seen: set[str] = set() + for item in strings: + value = item.value + if len(value) < 6 or len(value) > 512 or value in seen: + continue + if value.startswith(("L", "[")) and value.endswith(";"): + continue # type descriptors + if value.startswith("(") and ")" in value: + continue # method prototypes + if value.startswith("SMAP\n") or value.endswith((".kt", ".java")): + continue # Kotlin source maps and source file names + for tier, pattern in INTERESTING_STRING_TIERS: + if pattern.search(value): + seen.add(value) + buckets[tier].append( + { + "value": value, + "address": f"{item.dex}:0x{item.offset:x}", + "tier": tier, + } + ) + break + rows: list[dict[str, Any]] = [] + for tier in sorted(buckets): + rows.extend(buckets[tier]) + return rows[:limit] + + +def triage_json( + session: AscSession, + manifest: dict[str, Any], + reachable: dict[str, Any], + native_libs: list[dict[str, Any]], + entries: list[ApkEntry], +) -> dict[str, Any]: + packages: dict[str, int] = {} + for record in session.classes: + packages[record.package] = packages.get(record.package, 0) + 1 + top_packages = sorted(packages.items(), key=lambda pair: (-pair[1], pair[0]))[:25] + native_methods = [ + { + "name": method.descriptor, + "address": method.address, + "jni_symbol": jni_symbol(method), + } + for method in session.methods + if method.is_native + ] + return { + "binary_type": "apk", + "arch": "dalvik", + "bits": 32, + "package": manifest.get("package"), + "version_name": manifest.get("version_name"), + "version_code": manifest.get("version_code"), + "min_sdk": manifest.get("min_sdk"), + "target_sdk": manifest.get("target_sdk"), + "main_activities": manifest.get("main_activities", []), + "application_class": manifest.get("application", {}).get("android:name"), + "debuggable": manifest.get("application", {}).get("android:debuggable"), + "allow_backup": manifest.get("application", {}).get("android:allowBackup"), + "uses_cleartext_traffic": manifest.get("application", {}).get( + "android:usesCleartextTraffic" + ), + "network_security_config": manifest.get("application", {}).get( + "android:networkSecurityConfig" + ), + "dangerous_permissions": manifest.get("dangerous_permissions", []), + "permission_count": len(manifest.get("permissions", [])), + "component_counts": _component_counts(manifest.get("components", [])), + "exported_components": [ + {"kind": item["kind"], "name": item["name"]} + for item in manifest.get("exported_components", []) + ], + "dex_files": [ + {"name": image.name, "size": image.size, "apk": image.source_apk.name} + for image in session.images + ], + "class_count": len(session.classes), + "method_count": len(session.methods), + "string_count": len(session.strings), + "top_packages": [ + {"package": name or "", "classes": count} + for name, count in top_packages + ], + "native_libs": [ + { + "abi": item["abi"], + "name": item["name"], + "status": item["status"], + "export_dir": item.get("export_dir"), + } + for item in native_libs + ], + "native_methods": native_methods, + "assets": [item.name for item in entries if item.kind == "asset"][:100], + "strings_of_interest": interesting_strings(session.strings), + "reachable_count": len(reachable.get("reachable", [])), + "unreachable_count": reachable.get("unreachable_count", 0), + } + + +def _component_counts(components: list[dict[str, Any]]) -> dict[str, int]: + counts: dict[str, int] = {} + for item in components: + counts[item["kind"]] = counts.get(item["kind"], 0) + 1 + return counts + + +def package_graph_json(session: AscSession) -> dict[str, Any]: + by_id = {method.id: method for method in session.methods} + calls: dict[str, dict[str, int]] = {} + external: dict[str, dict[str, int]] = {} + class_counts: dict[str, int] = {} + for record in session.classes: + class_counts[record.package] = class_counts.get(record.package, 0) + 1 + for method in session.methods: + source = _package_of(method.class_descriptor) + for callee in method.callees: + target = _package_of(by_id[callee].class_descriptor) + if target != source: + bucket = calls.setdefault(source, {}) + bucket[target] = bucket.get(target, 0) + 1 + for descriptor in method.external_calls: + target = _package_of(descriptor.split("->", 1)[0]) + bucket = external.setdefault(source, {}) + bucket[target] = bucket.get(target, 0) + 1 + packages = [] + for name in sorted(class_counts): + packages.append( + { + "package": name or "", + "folder": f"src/raw/{name.replace('.', '/')}" if name else "src/raw", + "classes": class_counts[name], + "calls_packages": [ + {"package": target or "", "calls": count} + for target, count in sorted( + calls.get(name, {}).items(), key=lambda pair: -pair[1] + ) + ], + "calls_external": [ + {"package": target, "calls": count} + for target, count in sorted( + external.get(name, {}).items(), key=lambda pair: -pair[1] + )[:50] + ], + } + ) + return {"packages": packages} + + +def _package_of(descriptor: str) -> str: + name = java_type(descriptor) + return name.rsplit(".", 1)[0] if "." in name else "" + + +# -------------------------------------------------------------------------- +# Generated AGENTS.md +# -------------------------------------------------------------------------- + + +def build_apk_agents(*, package: str, native: bool, native_count: int) -> str: + lines = [ + "# AGENTS", + "", + f"You are working inside a ToCode APK export of `{package}`.", + "Treat the recovered Java, the manifest, the metadata, and the extracted resources as evidence for reverse engineering.", + "", + "## Mission", + "", + "Reverse the app, answer user questions, or serve as an oracle/helper to the user regarding this recovered source code.", + "Write a report only when the user asks for one; otherwise keep findings focused on the question or task at hand.", + "Do not refactor or modify the generated export unless the user explicitly asks for edits.", + "Use subagents only for narrow evidence-gathering tasks such as strings, component entry paths, or one package family.", + "", + "## Files", + "", + "- `src/raw//.java`: decompiled Java (ASC + androguard DAD), one file per class, folders follow Java packages. Inner classes are `Outer$Inner.java` next to their outer class.", + "- `AndroidManifest.xml`: decoded manifest; `manifest.json`: parsed package, versions, SDKs, permissions, components with intent filters and exported state, application attributes, split manifests.", + "- `classes.json`: every class with superclass, interfaces, access flags, fields, methods (with `address`), source file, and Java file/line ranges.", + "- `functions.json`: per-method callers/callees, external (framework/library) calls, string/field/type references, prototypes, and source line ranges. `address` is `:`.", + "- `function-index.json`: exact Java file and line range for each method.", + "- `strings.json`: DEX string pool with the methods that load each string.", + "- `imports.json`: methods called but not defined in any DEX (Android framework, JDK, missing libraries), grouped by package.", + "- `exports.json`: exported manifest components and JNI `native` methods with their expected `Java_...` symbol.", + "- `reachable.json`: methods reachable from manifest components (Application, activities, services, receivers, providers) with call depth.", + "- `package-graph.json`: inter-package call graph.", + "- `sections.json`: every entry of the APK set (dex, native libs, resources, assets) with sizes.", + "- `triage.json`: first-read summary: package, entry points, dangerous permissions, exported components, native libs and methods, strings of interest, reachability counts.", + "- `native-libs.json`: every shared object found in the APK set with ABI, hash, and the status of its native decompilation.", + "- `lib//*.so`: extracted native libraries.", + ] + if native: + lines.append( + f"- `native///`: a complete nested ToCode export for each of the {native_count} native libraries (own `AGENTS.md`, `src/raw/*.c`, `functions.json`, `exports.json`, ...). Pair `Java___` exports there with the `native` methods listed in `exports.json` here.", + ) + else: + lines.append( + "- `native/`: not generated (`--no-native`); native libraries are only extracted under `lib/`.", + ) + lines.extend( + [ + "- `data/apk/**`: every non-code APK entry verbatim (assets, res, META-INF, resources.arsc).", + "- `data/res/**/*.xml`: binary XML resources decoded to text; `data/resources.json`: decoded `resources.arsc` string/resource tables.", + "- `project.json` and `export-manifest.json`: top-level export paths and artifact inventory.", + "- `tocode.log`: export history including native library decompilation status.", + "- `CLAUDE.md`: Claude entrypoint that references `AGENTS.md`.", + "", + "## Working Style", + "", + "- Start with `triage.json`, `manifest.json`, `strings.json`, and `reachable.json`.", + "- Use `functions.json` and `function-index.json` to open only the method line ranges you need.", + "- Follow `native` methods into `native///` exports when the logic lives in JNI code.", + "- Treat obfuscated names (`a.b.c`) as opaque; rely on string, field, and framework references to recover intent.", + "- Cite file paths, class and method names, and line numbers for every major claim.", + "- Be explicit about uncertainty, decompilation failures (`classes.json` `decompile_error`), and unresolved references.", + "", + ] + ) + return "\n".join(lines) diff --git a/src/tocode/apk_native.py b/src/tocode/apk_native.py new file mode 100644 index 0000000..bbb3b3a --- /dev/null +++ b/src/tocode/apk_native.py @@ -0,0 +1,613 @@ +"""Native shared-object handling for APK exports. + +Every ``.so`` found in the APK set is extracted to ``lib//`` and, unless +``--no-native`` is given, decompiled with the regular ToCode pipeline +(``create_analyzer`` + ``export_binary``) into ``native///``. The +native work runs on a background thread so it overlaps with the DEX export; +one library failing never fails the APK export. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import multiprocessing +import os +from pathlib import Path +import queue as queue_mod +import re +import signal +import threading +import time +from typing import Any, Callable +import zipfile + +from .backends.base import BackendRequest +from .errors import ToCodeError +from .naming import clean_path_component +from .progress import Progress + + +ELF_MAGIC = b"\x7fELF" +# A decompiler backend needs room to analyze a library (r2's ``aaa`` on a few MB +# of ARM code can reach a gigabyte). The native thread runs next to the DEX +# decompile pool, so it waits for this much free memory before starting the next +# library instead of competing for the last megabytes and getting the whole run +# OOM-killed. The DEX side finishing is what usually releases it. +DEFAULT_NATIVE_MIN_FREE_MB = 1024 +NATIVE_MEMORY_POLL_SECONDS = 5.0 +NATIVE_MEMORY_MAX_WAIT_SECONDS = 1800.0 +# The nested export writes its own tocode.log; its lines are mirrored to the main +# output at this cadence so the terminal shows what the native side is doing. +NATIVE_LOG_TAIL_SECONDS = 0.5 +NATIVE_HEARTBEAT_SECONDS = 20.0 +_LOG_TIMESTAMP_RX = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\s+") +_RENDER_TOTAL_RX = re.compile(r"Rendering and writing (\d+) functions") +# Per-function render lines: "export 0x - bytes done|failed: ...". +# The parent also logs a bare "export 0x - bytes" when it hands a +# function to a worker; only completed ones count toward the bar. +_FUNCTION_LINE_RX = re.compile(r"^export \S+ 0x[0-9a-f]+ - \d+ bytes (done|failed)") +_FUNCTION_START_RX = re.compile(r"^export \S+ 0x[0-9a-f]+ - \d+ bytes$") +KNOWN_ABIS = ("arm64-v8a", "armeabi-v7a", "armeabi", "x86_64", "x86", "mips", "mips64") + + +@dataclass(slots=True) +class NativeLib: + abi: str + name: str + entry: str + source_apk: str + size: int + sha256: str + path: Path + export_dir: Path | None = None + backend: str | None = None + decompiler: str | None = None + function_count: int | None = None + failure_count: int | None = None + status: str = "pending" + seconds: float | None = None + # Package of the APK the library was extracted from (for the nested AGENTS.md). + package: str = "" + + @property + def stem(self) -> str: + return clean_path_component(Path(self.name).stem) + + def to_json(self, root: Path) -> dict[str, Any]: + return { + "abi": self.abi, + "name": self.name, + "entry": self.entry, + "source_apk": self.source_apk, + "size": self.size, + "sha256": self.sha256, + "path": _rel(self.path, root), + "export_dir": _rel(self.export_dir, root), + "backend": self.backend, + "decompiler": self.decompiler, + "function_count": self.function_count, + "failure_count": self.failure_count, + "status": self.status, + "seconds": round(self.seconds, 1) if self.seconds is not None else None, + } + + +@dataclass(slots=True) +class NativeOptions: + backend: BackendRequest = "auto" + analysis_command: str = "aaa" + idadir: Path | None = None + ida_domain_path: Path | None = None + purge_cache: bool = False + jobs: int | None = None + tree: bool = False + entropy: bool = False + + +def _rel(path: Path | None, root: Path) -> str | None: + if path is None: + return None + try: + return path.relative_to(root).as_posix() + except ValueError: + return str(path) + + +def classify_native_entry(name: str, head: bytes) -> tuple[str, str] | None: + """Return ``(abi, file name)`` when a zip entry is a shared object.""" + parts = name.split("/") + leaf = parts[-1] + if not leaf: + return None + is_so = leaf.endswith(".so") or ".so." in leaf + if len(parts) >= 3 and parts[0] == "lib": + if is_so or head.startswith(ELF_MAGIC): + return parts[1], leaf + if head.startswith(ELF_MAGIC) and (is_so or parts[0] in {"assets", "res"}): + abi = next((part for part in parts if part in KNOWN_ABIS), "unknown") + return abi, leaf + return None + + +def native_lib_path(root: Path, abi: str, name: str) -> Path: + return root / "lib" / clean_path_component(abi) / name + + +def native_export_dir(root: Path, lib: NativeLib) -> Path: + return root / "native" / clean_path_component(lib.abi) / lib.stem + + +NativeExporter = Callable[[NativeLib, Path, Progress], tuple[str, str, int, int]] + + +def describe_origin(lib: NativeLib) -> str: + """Provenance paragraph for the nested export's AGENTS.md.""" + package = f"`{lib.package}`" if lib.package else "an Android app" + return ( + f"This is a decompiled native library from the {package} APK: " + f"`{lib.name}` ({lib.abi}, `{lib.entry}` in `{lib.source_apk}`, " + f"sha256 `{lib.sha256}`). " + "The APK export that owns it is three directories up (`../../..`): its " + "`exports.json` and `triage.json` list the Java `native` methods with the " + "`Java___` JNI symbols this library is expected to " + "export, `native-libs.json` lists the other libraries of the app, and " + "`src/raw//` holds the decompiled Java that calls into this code." + ) + + +def run_native_export( + lib: NativeLib, out_dir: Path, options: NativeOptions, progress: Progress +) -> tuple[str, str, int, int]: + """Export one shared object with the regular binary pipeline.""" + from .analysis import create_analyzer + from .exporter import export_binary + + with create_analyzer( + lib.path, + backend=options.backend, + analysis_command=options.analysis_command, + progress=progress, + idadir=options.idadir, + ida_domain_path=options.ida_domain_path, + purge_cache=options.purge_cache, + ) as analyzer: + summary = export_binary( + analyzer, + out_dir=out_dir, + progress=progress, + jobs=options.jobs, + tree=options.tree, + entropy=options.entropy, + origin=describe_origin(lib), + ) + return ( + analyzer.backend_name, + analyzer.decompiler_label, + summary.function_count, + len(summary.failed_functions), + ) + + +def _native_export_child( + lib: NativeLib, + out_dir: Path, + options: NativeOptions, + log_path: Path, + result_queue: Any, +) -> None: + """Entry point of the isolated per-library export process.""" + # Lead a process group of our own: the backend's worker pool (grandchildren + # of the APK export) can then be killed as one unit if this process dies. + if hasattr(os, "setsid"): + try: + os.setsid() + except OSError: # pragma: no cover - already a session leader + pass + progress = Progress(enabled=False) + progress.set_log_path(log_path) + _redirect_output(log_path) + try: + result = run_native_export(lib, out_dir, options, progress) + except ToCodeError as exc: + result_queue.put(("error", str(exc))) + except BaseException as exc: # pragma: no cover - backend crash isolation + result_queue.put(("error", f"{type(exc).__name__}: {exc}")) + else: + result_queue.put(("ok", result)) + + +_active_children: set[int] = set() +_active_lock = threading.Lock() + + +def kill_process_group(pid: int | None) -> None: + """Kill the process group led by ``pid`` (no-op where unsupported). + + An OOM-killed or interrupted native child leaves its decompiler workers + behind; they hold the multiprocessing resource tracker open and the APK + export would then hang at exit waiting for it. + """ + if pid is None or not hasattr(os, "killpg"): + return + try: + os.killpg(pid, signal.SIGKILL) + except (ProcessLookupError, PermissionError, OSError): + pass + + +def terminate_active_native_children() -> None: + with _active_lock: + pids = list(_active_children) + for pid in pids: + kill_process_group(pid) + + +def collect_child_result( + process: Any, result_queue: Any, *, poll_seconds: float = 1.0 +) -> tuple[str, str, int, int]: + """Wait for an isolated export process and turn its outcome into a result. + + A missing outcome means the child died without reporting: a negative exit + code is a signal (the OOM killer, typically), which becomes a per-library + ``ToCodeError`` instead of taking the APK export down. Whatever the child + left running is killed with its process group. + """ + outcome: tuple[str, Any] | None = None + try: + while True: + try: + outcome = result_queue.get(timeout=poll_seconds) + break + except queue_mod.Empty: + if not process.is_alive(): + break + finally: + process.join() + if process.pid is not None: + with _active_lock: + _active_children.discard(process.pid) + if outcome is None: + kill_process_group(process.pid) + code = process.exitcode + if code is not None and code < 0: + raise ToCodeError( + f"export process was killed by signal {-code} " + "(out of memory?); try -j 1 or --no-native" + ) + raise ToCodeError(f"export process exited with code {code}") + kind, payload = outcome + if kind == "error": + raise ToCodeError(str(payload)) + backend, decompiler, functions, failures = payload + return (str(backend), str(decompiler), int(functions), int(failures)) + + +def _redirect_output(log_path: Path) -> None: + """Send the backend's own stdout/stderr chatter into the nested log.""" + try: + handle = open(log_path, "ab", buffering=0) + os.dup2(handle.fileno(), 1) + os.dup2(handle.fileno(), 2) + except OSError: # pragma: no cover - keep the terminal if the log is unusable + return + + +def default_native_exporter(options: NativeOptions) -> NativeExporter: + """Run each library's export in its own process. + + A decompiler backend can use more memory than the host has (r2's ``aaa`` on + a large library), and it may be OOM-killed or crash. In a child process that + only fails that library; in-process it would take the whole APK export -- and + the finished DEX work -- down with it. + """ + + def run( + lib: NativeLib, out_dir: Path, progress: Progress + ) -> tuple[str, str, int, int]: + context = multiprocessing.get_context("spawn") + result_queue: Any = context.Queue() + process = context.Process( + target=_native_export_child, + args=( + lib, + out_dir, + options, + _need_log_path(progress, out_dir), + result_queue, + ), + name=f"tocode-native-{lib.stem}", + ) + process.start() + if process.pid is not None: + with _active_lock: + _active_children.add(process.pid) + return collect_child_result(process, result_queue) + + return run + + +def _need_log_path(progress: Progress, out_dir: Path) -> Path: + return ( + progress.log_path if progress.log_path is not None else out_dir / "tocode.log" + ) + + +class NestedLogTail: + """Mirror a nested export's ``tocode.log`` into the main progress. + + Per-function render lines drive a progress bar instead of being echoed + (there are thousands); every other line is shown as ``native[lib]: ...``. + """ + + def __init__( + self, + log_path: Path, + lib: NativeLib, + progress: Progress, + *, + poll_seconds: float = NATIVE_LOG_TAIL_SECONDS, + ) -> None: + self.log_path = log_path + self.lib = lib + self.progress = progress + self.poll_seconds = poll_seconds + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._offset = 0 + self._bar_cm: Any = None + self._bar: Any = None + self.rendered = 0 + self.total: int | None = None + self.last_line: str = "starting" + + def start(self) -> None: + self._thread = threading.Thread( + target=self._run, name=f"tocode-native-tail-{self.lib.stem}", daemon=True + ) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join() + self._drain() + self._close_bar() + + def _run(self) -> None: + while not self._stop.is_set(): + self._drain() + self._stop.wait(self.poll_seconds) + + def _drain(self) -> None: + try: + with self.log_path.open("rb") as handle: + handle.seek(self._offset) + chunk = handle.read() + except OSError: + return + if not chunk: + return + # Only consume complete lines; keep a partial tail for the next poll. + end = chunk.rfind(b"\n") + if end < 0: + return + self._offset += end + 1 + for raw in chunk[: end + 1].decode("utf-8", errors="replace").splitlines(): + self._handle_line(_LOG_TIMESTAMP_RX.sub("", raw).rstrip()) + + def _handle_line(self, line: str) -> None: + if not line: + return + if _FUNCTION_LINE_RX.match(line): + self.rendered += 1 + if self._bar is not None: + self._bar.update(1) + return + if _FUNCTION_START_RX.match(line): + return + self.last_line = line + total = _RENDER_TOTAL_RX.search(line) + if total is not None: + self.total = int(total.group(1)) + self._open_bar() + self.progress.log(f"native[{self.lib.name}]: {line}") + + def _open_bar(self) -> None: + if self._bar is not None or self.total is None: + return + self._bar_cm = self.progress.bar( + total=self.total, desc=f"native {self.lib.name}", unit="fn" + ) + self._bar = self._bar_cm.__enter__() + if self.rendered: + self._bar.update(self.rendered) + + def _close_bar(self) -> None: + if self._bar_cm is not None: + self._bar_cm.__exit__(None, None, None) + self._bar_cm = None + self._bar = None + + def status(self) -> str: + if self.total: + return f"{self.rendered}/{self.total} functions" + return self.last_line + + +@dataclass +class NativeRunner: + """Runs the native exports on a background thread.""" + + libs: list[NativeLib] + root: Path + progress: Progress + exporter: NativeExporter + stop_event: threading.Event = field(default_factory=threading.Event) + thread: threading.Thread | None = None + errors: list[str] = field(default_factory=list) + min_free_mb: int = 0 + available_memory: Callable[[], int | None] = lambda: None + poll_seconds: float = NATIVE_MEMORY_POLL_SECONDS + max_wait_seconds: float = NATIVE_MEMORY_MAX_WAIT_SECONDS + mirror_logs: bool = True + current: NativeLib | None = None + current_started: float = 0.0 + current_tail: NestedLogTail | None = None + + def describe_current(self) -> str | None: + """One line about what the native thread is doing right now.""" + lib = self.current + if lib is None: + return None + elapsed = time.monotonic() - self.current_started + detail = self.current_tail.status() if self.current_tail else "running" + done = sum(1 for item in self.libs if item.status == "done") + finished = sum(1 for item in self.libs if item.status not in {"pending"}) + return ( + f"native: {lib.name} ({lib.abi}) {detail}, {elapsed:.0f}s elapsed; " + f"{finished}/{len(self.libs)} libraries finished, {done} exported" + ) + + def start(self) -> None: + self.thread = threading.Thread( + target=self._run, name="tocode-native", daemon=True + ) + self.thread.start() + + def join(self) -> None: + if self.thread is not None: + self.thread.join() + + def stop(self) -> None: + self.stop_event.set() + terminate_active_native_children() + + def _run(self) -> None: + for lib in self.libs: + if self.stop_event.is_set(): + lib.status = "skipped: interrupted" + continue + self._wait_for_memory(lib) + if self.stop_event.is_set(): + lib.status = "skipped: interrupted" + continue + self._export_one(lib) + + def _wait_for_memory(self, lib: NativeLib) -> None: + if self.min_free_mb <= 0: + return + waited = 0.0 + logged = False + while not self.stop_event.is_set() and waited < self.max_wait_seconds: + available = self.available_memory() + if available is None or available >= self.min_free_mb: + break + if not logged: + self.progress.log( + f"native: waiting for memory before {lib.name} ({lib.abi}): " + f"{available} MB free, need {self.min_free_mb} MB" + ) + logged = True + self.stop_event.wait(self.poll_seconds) + waited += self.poll_seconds + if logged and not self.stop_event.is_set(): + self.progress.file_log( + f"native: waited {waited:.0f}s for memory before {lib.name}" + ) + + def _export_one(self, lib: NativeLib) -> None: + out_dir = native_export_dir(self.root, lib) + lib.export_dir = out_dir + log_path = out_dir / "tocode.log" + child = Progress(enabled=False) + child.set_log_path(log_path) + self.progress.log( + f"native: {lib.name} ({lib.abi}) export started -> " + f"{_rel(out_dir, self.root)}" + ) + started = time.monotonic() + tail = NestedLogTail(log_path, lib, self.progress) if self.mirror_logs else None + self.current = lib + self.current_started = started + self.current_tail = tail + if tail is not None: + tail.start() + try: + backend, decompiler, functions, failures = self.exporter( + lib, out_dir, child + ) + except ToCodeError as exc: + lib.status = f"failed: {exc}" + except Exception as exc: # pragma: no cover - backend crash isolation + lib.status = f"failed: {type(exc).__name__}: {exc}" + else: + lib.backend = backend + lib.decompiler = decompiler + lib.function_count = functions + lib.failure_count = failures + lib.status = "done" + finally: + if tail is not None: + tail.stop() + self.current = None + self.current_tail = None + lib.seconds = time.monotonic() - started + if lib.status == "done": + self.progress.log( + f"native: {lib.name} ({lib.abi}) exported with {lib.backend} " + f"in {lib.seconds:.1f}s: functions={lib.function_count} " + f"failures={lib.failure_count}" + ) + else: + self.errors.append(f"{lib.abi}/{lib.name}: {lib.status}") + self.progress.log(f"native: {lib.name} ({lib.abi}) {lib.status}") + + +def extract_native_libs( + apks: list[Path], root: Path, *, sha256: Callable[[Path], str] +) -> list[NativeLib]: + """Extract every shared object of the APK set to ``lib//``.""" + libs: list[NativeLib] = [] + seen: set[tuple[str, str]] = set() + for apk in apks: + with zipfile.ZipFile(apk) as archive: + for info in archive.infolist(): + if info.is_dir(): + continue + with archive.open(info) as handle: + head = handle.read(4) + classified = classify_native_entry(info.filename, head) + if classified is None: + continue + abi, name = classified + if (abi, name) in seen: + continue + seen.add((abi, name)) + target = native_lib_path(root, abi, name) + target.parent.mkdir(parents=True, exist_ok=True) + with archive.open(info) as source, target.open("wb") as sink: + while True: + chunk = source.read(1 << 20) + if not chunk: + break + sink.write(chunk) + libs.append( + NativeLib( + abi=abi, + name=name, + entry=info.filename, + source_apk=apk.name, + size=info.file_size, + sha256=sha256(target), + path=target, + ) + ) + return libs + + +def native_libs_json(libs: list[NativeLib], root: Path) -> dict[str, Any]: + return { + "count": len(libs), + "done": sum(1 for item in libs if item.status == "done"), + "libs": [item.to_json(root) for item in libs], + } diff --git a/src/tocode/backends/asc.py b/src/tocode/backends/asc.py new file mode 100644 index 0000000..327b7e7 --- /dev/null +++ b/src/tocode/backends/asc.py @@ -0,0 +1,1012 @@ +"""ASC (droidasc) backend: APK/DEX inventory and per-class Java decompilation. + +ASC treats the APK as a read-only database, so this module keeps the same +shape: the parent process inflates every DEX once, walks the tinydex tables for +the inventory, and worker processes rebuild their own view of one DEX to +decompile classes. Nothing outside this module imports ``droidasc`` directly. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import hashlib +import importlib +import importlib.util +import mmap +from pathlib import Path +import struct +import sys +from typing import Any +import zipfile + +from ..errors import ToCodeError + + +APK_SUFFIXES = frozenset({".apk"}) +# Per-method/per-string reference caps. Both tables are unbounded in principle +# (a large APK has millions of edges), and the tail is not useful for triage. +MAX_STRING_XREFS = 64 +MAX_FIELD_REFS = 32 +APK_BUNDLE_SUFFIXES = frozenset({".apks", ".xapk"}) +NO_INDEX = 0xFFFFFFFF + +# Dalvik access flags (dex format spec). +ACC_PUBLIC = 0x1 +ACC_PRIVATE = 0x2 +ACC_PROTECTED = 0x4 +ACC_STATIC = 0x8 +ACC_FINAL = 0x10 +ACC_SYNCHRONIZED = 0x20 +ACC_VOLATILE = 0x40 +ACC_BRIDGE = 0x40 +ACC_TRANSIENT = 0x80 +ACC_VARARGS = 0x80 +ACC_NATIVE = 0x100 +ACC_INTERFACE = 0x200 +ACC_ABSTRACT = 0x400 +ACC_STRICT = 0x800 +ACC_SYNTHETIC = 0x1000 +ACC_ANNOTATION = 0x2000 +ACC_ENUM = 0x4000 +ACC_CONSTRUCTOR = 0x10000 +ACC_DECLARED_SYNCHRONIZED = 0x20000 + +_CLASS_FLAG_NAMES = ( + (ACC_PUBLIC, "public"), + (ACC_PRIVATE, "private"), + (ACC_PROTECTED, "protected"), + (ACC_STATIC, "static"), + (ACC_FINAL, "final"), + (ACC_INTERFACE, "interface"), + (ACC_ABSTRACT, "abstract"), + (ACC_SYNTHETIC, "synthetic"), + (ACC_ANNOTATION, "annotation"), + (ACC_ENUM, "enum"), +) +_METHOD_FLAG_NAMES = ( + (ACC_PUBLIC, "public"), + (ACC_PRIVATE, "private"), + (ACC_PROTECTED, "protected"), + (ACC_STATIC, "static"), + (ACC_FINAL, "final"), + (ACC_SYNCHRONIZED, "synchronized"), + (ACC_BRIDGE, "bridge"), + (ACC_VARARGS, "varargs"), + (ACC_NATIVE, "native"), + (ACC_ABSTRACT, "abstract"), + (ACC_STRICT, "strictfp"), + (ACC_SYNTHETIC, "synthetic"), + (ACC_CONSTRUCTOR, "constructor"), + (ACC_DECLARED_SYNCHRONIZED, "declared-synchronized"), +) +_FIELD_FLAG_NAMES = ( + (ACC_PUBLIC, "public"), + (ACC_PRIVATE, "private"), + (ACC_PROTECTED, "protected"), + (ACC_STATIC, "static"), + (ACC_FINAL, "final"), + (ACC_VOLATILE, "volatile"), + (ACC_TRANSIENT, "transient"), + (ACC_SYNTHETIC, "synthetic"), + (ACC_ENUM, "enum"), +) + +# ``droidasc.asc_core.utils.decompiler`` replaces modules that are not yet +# imported with dummies (to make androguard import faster). Anything ToCode or +# androguard needs afterwards must therefore be loaded for real first. +_PRELOAD_MODULES = ( + "json", + "math", + "random", + "tempfile", + "shutil", + "bisect", + "weakref", + "bz2", + "lzma", + "multiprocessing", + "multiprocessing.connection", + "xml.sax.saxutils", + "xml.dom.minidom", + "email", + "email.parser", + "urllib", + "urllib.request", + "http.client", + "lxml", + "lxml.etree", + "asn1crypto", + "asn1crypto.x509", + "cryptography", + "loguru", + "click", + "colorama", + "dateutil", + "urllib3", + "requests", + "idna", + "chardet", + "certifi", + "androguard.core.axml", +) + +_asc_cache: dict[str, Any] | None = None + + +def is_apk_bundle(path: Path) -> bool: + return path.suffix.lower() in APK_BUNDLE_SUFFIXES + + +def is_apk_input(path: Path) -> bool: + suffix = path.suffix.lower() + return suffix in APK_SUFFIXES or suffix in APK_BUNDLE_SUFFIXES + + +def probe_asc() -> bool: + return importlib.util.find_spec("droidasc") is not None + + +def preload_real_modules() -> None: + for name in _PRELOAD_MODULES: + if name in sys.modules: + continue + try: + importlib.import_module(name) + except Exception: # pragma: no cover - optional third-party modules + continue + + +def quiet_android_loggers() -> None: + """androguard logs every instruction at DEBUG through loguru; silence it.""" + try: + from loguru import logger + + logger.remove() + except Exception: # pragma: no cover - loguru always ships with androguard + pass + import logging + + for name in ("androguard", "asc", "droidasc"): + logging.getLogger(name).disabled = True + logging.getLogger(name).setLevel(logging.CRITICAL) + + +def asc_modules() -> dict[str, Any]: + """Import droidasc lazily (it is slow and patches androguard at import).""" + global _asc_cache + if _asc_cache is not None: + return _asc_cache + if not probe_asc(): + raise ToCodeError( + "APK input requires the ASC backend: install droidasc " + "(`pip install droidasc`)" + ) + preload_real_modules() + quiet_android_loggers() + apk_handler = importlib.import_module("droidasc.asc_client.apk_handler") + dex_container = importlib.import_module("droidasc.asc_client.dex_container") + manifest_handler = importlib.import_module("droidasc.asc_client.manifest_handler") + tinydex = importlib.import_module("droidasc.asc_core.utils.tinydex") + dvm_opcode = importlib.import_module("droidasc.asc_core.models.dvm_opcode") + _asc_cache = { + "apk_handler": apk_handler, + "dex_container": dex_container, + "manifest_handler": manifest_handler, + "tinydex": tinydex, + "dvm_opcode": dvm_opcode, + } + return _asc_cache + + +def _decompiler_modules() -> tuple[Any, Any]: + mods = asc_modules() + if "dex_manager" not in mods: + preload_real_modules() + quiet_android_loggers() + mods["dex_manager"] = importlib.import_module( + "droidasc.asc_core.core.dex.dex_manager" + ) + mods["decompiler"] = importlib.import_module( + "droidasc.asc_core.utils.decompiler" + ) + return mods["dex_manager"], mods["decompiler"] + + +# -------------------------------------------------------------------------- +# APK set discovery (bundles and split APKs) +# -------------------------------------------------------------------------- + + +@dataclass(slots=True) +class ApkSet: + primary: Path + apks: list[Path] + workdir: Path | None = None + + @property + def is_split(self) -> bool: + return len(self.apks) > 1 + + +def discover_apk_set(path: Path, *, splits: bool = True, workdir: Path) -> ApkSet: + """Resolve the APK(s) that make up one app. + + ``.apks``/``.xapk`` bundles are unpacked into ``workdir`` and every inner + ``*.apk`` joins the set. A ``base.apk`` picks up sibling ``split_*.apk`` + files unless ``splits`` is off. Any other APK is exported on its own. + """ + path = Path(path).resolve() + if is_apk_bundle(path): + return _unpack_bundle(path, workdir) + if not zipfile.is_zipfile(path): + raise ToCodeError(f"not a valid APK (zip) file: {path}") + apks = [path] + if splits and path.name.lower() == "base.apk": + siblings = sorted( + item + for item in path.parent.glob("split_*.apk") + if item.is_file() and zipfile.is_zipfile(item) + ) + apks.extend(siblings) + return ApkSet(primary=path, apks=apks) + + +def _unpack_bundle(path: Path, workdir: Path) -> ApkSet: + try: + archive = zipfile.ZipFile(path) + except zipfile.BadZipFile as exc: + raise ToCodeError(f"not a valid APK bundle (zip) file: {path}") from exc + extracted: list[Path] = [] + with archive: + for info in archive.infolist(): + name = Path(info.filename).name + if "/" in info.filename.strip("/") or not name.lower().endswith(".apk"): + continue + target = workdir / name + target.parent.mkdir(parents=True, exist_ok=True) + with archive.open(info) as source, target.open("wb") as sink: + while True: + chunk = source.read(1 << 20) + if not chunk: + break + sink.write(chunk) + if zipfile.is_zipfile(target): + extracted.append(target) + if not extracted: + raise ToCodeError(f"APK bundle contains no .apk entries: {path}") + extracted.sort(key=_split_order) + return ApkSet(primary=extracted[0], apks=extracted, workdir=workdir) + + +def _split_order(path: Path) -> tuple[int, str]: + name = path.name.lower() + if name == "base.apk": + return (0, name) + if name.startswith("split_config."): + return (2, name) + return (1, name) + + +# -------------------------------------------------------------------------- +# DEX images and inventory records +# -------------------------------------------------------------------------- + + +@dataclass(slots=True) +class DexImage: + name: str + source_apk: Path + entry: str + data: bytes + dex: Any + + @property + def size(self) -> int: + return len(self.data) + + +@dataclass(slots=True) +class FieldRecord: + dex: str + index: int + class_descriptor: str + name: str + type: str + access_flags: int + + @property + def is_static(self) -> bool: + return bool(self.access_flags & ACC_STATIC) + + @property + def flag_names(self) -> list[str]: + return flag_names(self.access_flags, _FIELD_FLAG_NAMES) + + +@dataclass(slots=True) +class MethodRecord: + id: int + dex: str + index: int + class_descriptor: str + name: str + return_type: str + params: list[str] + access_flags: int + is_direct: bool + code_offset: int + code_size: int + registers: int = 0 + # Filled by the reference scan (worker side), resolved by the session. + callees: list[int] = field(default_factory=list) + external_calls: list[str] = field(default_factory=list) + # String references are kept once per string in ``AscSession.string_xrefs`` + # (a method-id list per string) rather than as a resolved list per method: + # a large APK has millions of such edges and the per-method copy dominated + # the exporter's memory. + string_ref_count: int = 0 + field_refs: list[str] = field(default_factory=list) + callers: list[int] = field(default_factory=list) + line_start: int | None = None + line_end: int | None = None + + @property + def descriptor(self) -> str: + return method_descriptor( + self.class_descriptor, self.name, self.params, self.return_type + ) + + @property + def is_native(self) -> bool: + return bool(self.access_flags & ACC_NATIVE) + + @property + def is_abstract(self) -> bool: + return bool(self.access_flags & ACC_ABSTRACT) + + @property + def is_static(self) -> bool: + return bool(self.access_flags & ACC_STATIC) + + @property + def flag_names(self) -> list[str]: + return flag_names(self.access_flags, _METHOD_FLAG_NAMES) + + @property + def address(self) -> str: + return ( + f"{self.dex}:0x{self.code_offset:x}" + if self.code_offset + else (f"{self.dex}:method@{self.index}") + ) + + @property + def prototype(self) -> str: + params = ", ".join(java_type(item) for item in self.params) + return f"{java_type(self.return_type)} {self.name}({params})" + + +@dataclass(slots=True) +class ClassRecord: + descriptor: str + dex: str + index: int + access_flags: int + superclass: str | None + interfaces: list[str] + source_file: str | None + methods: list[MethodRecord] + fields: list[FieldRecord] + java_path: Path | None = None + line_count: int = 0 + error: str | None = None + entry: bool = False + + @property + def java_name(self) -> str: + return java_type(self.descriptor) + + @property + def package(self) -> str: + name = self.java_name + return name.rsplit(".", 1)[0] if "." in name else "" + + @property + def simple_name(self) -> str: + return self.java_name.rsplit(".", 1)[-1] + + @property + def flag_names(self) -> list[str]: + return flag_names(self.access_flags, _CLASS_FLAG_NAMES) + + @property + def is_interface(self) -> bool: + return bool(self.access_flags & ACC_INTERFACE) + + @property + def has_native_methods(self) -> bool: + return any(item.is_native for item in self.methods) + + +@dataclass(slots=True) +class StringRecord: + dex: str + index: int + value: str + offset: int + + +@dataclass(slots=True) +class DecompiledClass: + descriptor: str + source: str | None + error: str | None + # method index -> list of (ref kind, index) pairs from the bytecode scan. + refs: dict[int, list[tuple[str, int]]] + + +def flag_names(flags: int, table: tuple[tuple[int, str], ...]) -> list[str]: + return [name for bit, name in table if flags & bit] + + +def java_type(descriptor: str) -> str: + """``Lcom/foo/Bar;`` -> ``com.foo.Bar``; ``[I`` -> ``int[]``.""" + dims = 0 + while descriptor.startswith("["): + dims += 1 + descriptor = descriptor[1:] + primitives = { + "V": "void", + "Z": "boolean", + "B": "byte", + "S": "short", + "C": "char", + "I": "int", + "J": "long", + "F": "float", + "D": "double", + } + if descriptor in primitives: + base = primitives[descriptor] + elif descriptor.startswith("L") and descriptor.endswith(";"): + base = descriptor[1:-1].replace("/", ".") + else: + base = descriptor + return base + "[]" * dims + + +def method_descriptor( + class_descriptor: str, name: str, params: list[str], return_type: str +) -> str: + return f"{class_descriptor}->{name}({''.join(params)}){return_type}" + + +def class_java_relpath(descriptor: str) -> Path: + """Package folders plus ``.java`` for a Dalvik class descriptor.""" + from ..naming import clean_path_component + + name = descriptor[1:-1] if descriptor.startswith("L") else descriptor + parts = name.split("/") + folders = [clean_path_component(item) for item in parts[:-1]] + leaf = ( + "".join( + ch if (ch.isascii() and (ch.isalnum() or ch in "_$")) else "_" + for ch in parts[-1] + ).strip("_") + or "unnamed" + ) + return Path(*folders, f"{leaf}.java") if folders else Path(f"{leaf}.java") + + +# -------------------------------------------------------------------------- +# DEX loading +# -------------------------------------------------------------------------- + + +def load_dex_images(apk: Path, *, prefix: str = "") -> list[DexImage]: + """Inflate every ``classes*.dex`` (and v41 container members) of one APK.""" + mods = asc_modules() + apk_handler = mods["apk_handler"] + dex_container = mods["dex_container"] + tinydex = mods["tinydex"] + images: list[DexImage] = [] + with open(apk, "rb") as handle: + try: + view = mmap.mmap(handle.fileno(), 0, access=mmap.ACCESS_READ) + except ValueError: + return images # empty file + try: + entries = apk_handler._parse_cd_dex_entries(view) + for entry in entries: + try: + data = bytes(apk_handler._inflate_dex(view, entry)) + except Exception as exc: + raise ToCodeError(f"cannot inflate {entry[0]} in {apk}: {exc}") + for name, buf in dex_container.iter_logical_dex_buffers(entry[0], data): + payload = bytes(buf) + images.append( + DexImage( + name=f"{prefix}{name}", + source_apk=apk, + entry=entry[0], + data=payload, + dex=tinydex.DEX(payload, name), + ) + ) + finally: + view.close() + return images + + +def load_apk_set_images(apk_set: ApkSet) -> list[DexImage]: + """DEX images across the whole set; names stay unique across APKs.""" + images: list[DexImage] = [] + seen: set[str] = set() + for apk in apk_set.apks: + prefix = "" if apk == apk_set.primary else f"{apk.name}!" + for image in load_dex_images(apk, prefix=prefix): + if image.name in seen: + image.name = f"{apk.name}!{image.name}" + seen.add(image.name) + images.append(image) + return images + + +def manifest_xml(apk: Path) -> str: + mods = asc_modules() + try: + return str(mods["manifest_handler"].get_manifest_xml(str(apk), pretty=True)) + except KeyError as exc: + raise ToCodeError(f"{apk.name} has no AndroidManifest.xml") from exc + except Exception as exc: + raise ToCodeError(f"cannot decode AndroidManifest.xml in {apk.name}: {exc}") + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + +# -------------------------------------------------------------------------- +# Bytecode reference scan +# -------------------------------------------------------------------------- + +REF_STRING = "string" +REF_TYPE = "type" +REF_FIELD = "field" +REF_METHOD = "method" + +_U16 = struct.Struct(" list[tuple[str, int]]: + """Return every (kind, index) the instruction stream references. + + Mirrors ASC's ``IndexHandler``: walks the opcode table, stops at the first + switch/array-data payload, and reads the constant-pool index of the + string/type/field/method instruction formats. + """ + if opcodes is None: + opcodes = asc_modules()["dvm_opcode"] + table = opcodes.opcodes + index_flag = opcodes.IndexFlag + verify_flag = opcodes.VerifyFlag + fmt_k31c = opcodes.Format.k31c + payload_mask = verify_flag.kVerifySwitchTargets | verify_flag.kVerifyArrayData + kinds = { + index_flag.kIndexStringRef: REF_STRING, + index_flag.kIndexTypeRef: REF_TYPE, + index_flag.kIndexFieldRef: REF_FIELD, + index_flag.kIndexMethodRef: REF_METHOD, + index_flag.kIndexMethodAndProtoRef: REF_METHOD, + } + data = bytes(bytecode) + refs: list[tuple[str, int]] = [] + length = len(data) + bound = length + pc = 0 + while pc + 1 < length and pc < bound: + op = table.get(data[pc]) + if op is None: + break + kind = kinds.get(op.idx) + if kind is not None and pc + 4 <= length: + if op.fmt == fmt_k31c: + if pc + 6 <= length: + refs.append((kind, _U32.unpack_from(data, pc + 2)[0])) + else: + refs.append((kind, _U16.unpack_from(data, pc + 2)[0])) + elif op.vflag & payload_mask and pc + 6 <= length: + target = pc + _I32.unpack_from(data, pc + 2)[0] * 2 + if 0 < target < bound: + bound = target + pc += op.oplen * 2 + return refs + + +# -------------------------------------------------------------------------- +# Session: inventory over loaded DEX images +# -------------------------------------------------------------------------- + + +class AscSession: + backend_name = "asc" + backend_label = "ASC" + decompiler_label = "ASC (droidasc) + androguard DAD" + + def __init__(self, apk_set: ApkSet) -> None: + self.apk_set = apk_set + self.images: list[DexImage] = [] + self.classes: list[ClassRecord] = [] + self.methods: list[MethodRecord] = [] + self.strings: list[StringRecord] = [] + self._by_descriptor: dict[str, ClassRecord] = {} + self._method_by_descriptor: dict[str, MethodRecord] = {} + self._images_by_name: dict[str, DexImage] = {} + self._pool_cache: dict[tuple[str, str, int], str] = {} + # (dex name, string index) -> ids of the methods that load it (capped). + self.string_xrefs: dict[tuple[str, int], list[int]] = {} + + # -- loading ----------------------------------------------------------- + + def load(self) -> None: + self.images = load_apk_set_images(self.apk_set) + self._images_by_name = {image.name: image for image in self.images} + self.classes = [] + self.methods = [] + self.strings = [] + next_id = 0 + for image in self.images: + dex = image.dex + for class_index in range(len(dex.classes)): + cls = dex.classes[class_index] + record, next_id = self._class_record(image, cls, next_id) + self.classes.append(record) + self._by_descriptor[record.descriptor] = record + for method in record.methods: + self.methods.append(method) + self._method_by_descriptor[method.descriptor] = method + self.strings.extend(self._string_records(image)) + + def _class_record( + self, image: DexImage, cls: Any, next_id: int + ) -> tuple[ClassRecord, int]: + dex = image.dex + ( + _class_idx, + access_flags, + superclass_idx, + interfaces_off, + source_file_idx, + _annotations_off, + _class_data_off, + _static_values_off, + ) = struct.unpack_from("<8I", dex.buf, cls._class_def_off) + superclass = ( + self._type_name(image, superclass_idx) + if superclass_idx != NO_INDEX + else None + ) + interfaces: list[str] = [] + if interfaces_off: + count = _U32.unpack_from(dex.buf, interfaces_off)[0] + for i in range(count): + type_idx = _U16.unpack_from(dex.buf, interfaces_off + 4 + i * 2)[0] + interfaces.append(self._type_name(image, type_idx)) + source_file = ( + self._string_value(image, source_file_idx) + if source_file_idx != NO_INDEX + else None + ) + descriptor = cls.fullname + methods: list[MethodRecord] = [] + for method in cls.methods: + proto = method.prototype + code_size = 0 + registers = 0 + code_off = method.code_offset + if code_off: + registers, _ins, _outs, _tries, _dbg, insns = struct.unpack_from( + " list[StringRecord]: + dex = image.dex + count = len(dex.strings) + base = dex.header.strings[0] + records: list[StringRecord] = [] + for index in range(count): + offset = _U32.unpack_from(dex.buf, base + index * 4)[0] + try: + value = dex.strings[index] + except Exception: + continue + records.append(StringRecord(image.name, index, str(value), offset)) + return records + + # -- lookups ----------------------------------------------------------- + + def image(self, name: str) -> DexImage: + return self._images_by_name[name] + + def class_by_descriptor(self, descriptor: str) -> ClassRecord | None: + return self._by_descriptor.get(descriptor) + + def method_by_descriptor(self, descriptor: str) -> MethodRecord | None: + return self._method_by_descriptor.get(descriptor) + + def _type_name(self, image: DexImage, type_idx: int) -> str: + key = (image.name, REF_TYPE, type_idx) + cached = self._pool_cache.get(key) + if cached is None: + try: + cached = str(image.dex.types[type_idx].descriptor) + except Exception: + cached = f"" + self._pool_cache[key] = cached + return cached + + def _string_value(self, image: DexImage, string_idx: int) -> str: + key = (image.name, REF_STRING, string_idx) + cached = self._pool_cache.get(key) + if cached is None: + try: + cached = str(image.dex.strings[string_idx]) + except Exception: + cached = f"" + self._pool_cache[key] = cached + return cached + + def _method_name(self, image: DexImage, method_idx: int) -> str: + key = (image.name, REF_METHOD, method_idx) + cached = self._pool_cache.get(key) + if cached is None: + try: + method = image.dex.methods[method_idx] + proto = method.prototype + cached = method_descriptor( + method.cls.fullname, + method.name, + [item.descriptor for item in proto.parameters_type], + self._type_name(image, proto.return_type_idx), + ) + except Exception: + cached = f"" + self._pool_cache[key] = cached + return cached + + def _field_name(self, image: DexImage, field_idx: int) -> str: + key = (image.name, REF_FIELD, field_idx) + cached = self._pool_cache.get(key) + if cached is None: + try: + item = image.dex.fields[field_idx] + cached = f"{item.cls.fullname}->{item.name}:{item.type}" + except Exception: + cached = f"" + self._pool_cache[key] = cached + return cached + + # -- reference resolution --------------------------------------------- + + def apply_refs( + self, record: ClassRecord, refs: dict[int, list[tuple[str, int]]] + ) -> None: + """Resolve a worker's raw (kind, index) pairs into method records.""" + image = self._images_by_name.get(record.dex) + if image is None: + return + by_index = {method.index: method for method in record.methods} + for method_index, pairs in refs.items(): + method = by_index.get(method_index) + if method is None: + continue + callees: list[int] = [] + external: list[str] = [] + fields: list[str] = [] + string_count = 0 + seen: set[tuple[str, int]] = set() + for kind, index in pairs: + if (kind, index) in seen: + continue + seen.add((kind, index)) + if kind == REF_METHOD: + descriptor = self._method_name(image, index) + target = self._method_by_descriptor.get(descriptor) + if target is None: + external.append(descriptor) + else: + callees.append(target.id) + target.callers.append(method.id) + elif kind == REF_STRING: + string_count += 1 + bucket = self.string_xrefs.setdefault((record.dex, index), []) + if len(bucket) < MAX_STRING_XREFS: + bucket.append(method.id) + elif kind == REF_FIELD and len(fields) < MAX_FIELD_REFS: + fields.append(self._field_name(image, index)) + method.callees = callees + method.external_calls = external + method.string_ref_count = string_count + method.field_refs = fields + + def scan_refs_inline(self, record: ClassRecord) -> dict[int, list[tuple[str, int]]]: + """Reference scan in the parent (used when no worker pool runs).""" + image = self._images_by_name[record.dex] + cls = image.dex.classes[record.index] + result: dict[int, list[tuple[str, int]]] = {} + opcodes = asc_modules()["dvm_opcode"] + for method in cls.methods: + bytecode = method.bytecode + if bytecode: + result[method.index] = scan_bytecode_refs(bytecode, opcodes=opcodes) + return result + + def close(self) -> None: + self.images = [] + self._images_by_name = {} + self._pool_cache = {} + self.string_xrefs = {} + + +# -------------------------------------------------------------------------- +# Worker-side decompilation (spawned processes) +# -------------------------------------------------------------------------- + +_worker_state: dict[str, Any] = {} + + +def init_decompile_worker(apk_set: ApkSet) -> None: + """Load the DEX images of the set in this worker; managers are built lazily.""" + images = load_apk_set_images(apk_set) + dex_manager_mod, decompiler_mod = _decompiler_modules() + quiet_android_loggers() + _worker_state["images"] = {image.name: image for image in images} + _worker_state["managers"] = {} + _worker_state["manager_factory"] = dex_manager_mod.DexManager + _worker_state["decompile"] = decompiler_mod.decompile_dex_bytes + _worker_state["opcodes"] = asc_modules()["dvm_opcode"] + + +def _worker_manager(dex_name: str) -> tuple[DexImage, Any]: + image = _worker_state["images"][dex_name] + managers = _worker_state["managers"] + manager = managers.get(dex_name) + if manager is None: + manager = _worker_state["manager_factory"](memoryview(image.data)) + managers[dex_name] = manager + return image, manager + + +def decompile_batch_in_worker( + items: list[tuple[str, str, int]], +) -> list[DecompiledClass]: + """Decompile ``(dex name, descriptor, class index)`` items in this worker.""" + results: list[DecompiledClass] = [] + for dex_name, descriptor, class_index in items: + try: + image, manager = _worker_manager(dex_name) + except Exception as exc: + results.append( + DecompiledClass(descriptor, None, f"dex unavailable: {exc}", {}) + ) + continue + results.append( + decompile_class( + image, + descriptor, + class_index, + manager=manager, + decompile=_worker_state["decompile"], + opcodes=_worker_state["opcodes"], + ) + ) + return results + + +def decompile_class( + image: DexImage, + descriptor: str, + class_index: int, + *, + manager: Any, + decompile: Any, + opcodes: Any, +) -> DecompiledClass: + refs: dict[int, list[tuple[str, int]]] = {} + try: + cls = image.dex.classes[class_index] + for method in cls.methods: + bytecode = method.bytecode + if bytecode: + refs[method.index] = scan_bytecode_refs(bytecode, opcodes=opcodes) + except Exception as exc: # pragma: no cover - defensive, tinydex is stable + return DecompiledClass(descriptor, None, f"reference scan failed: {exc}", {}) + try: + rebuilt = manager.extract_and_rebuild(descriptor) + source = decompile(bytes(rebuilt), descriptor) + except Exception as exc: + return DecompiledClass(descriptor, None, f"{type(exc).__name__}: {exc}", refs) + if not isinstance(source, str) or source.startswith("Error:"): + return DecompiledClass(descriptor, None, str(source).strip() or "empty", refs) + return DecompiledClass(descriptor, source, None, refs) + + +class InlineDecompiler: + """In-process decompiler used for the serial path (and by tests).""" + + def __init__(self, session: AscSession) -> None: + self.session = session + self._managers: dict[str, Any] = {} + self._factory: Any = None + self._decompile: Any = None + self._opcodes: Any = None + + def _ensure(self) -> None: + if self._factory is None: + dex_manager_mod, decompiler_mod = _decompiler_modules() + self._factory = dex_manager_mod.DexManager + self._decompile = decompiler_mod.decompile_dex_bytes + self._opcodes = asc_modules()["dvm_opcode"] + + def decompile(self, record: ClassRecord) -> DecompiledClass: + self._ensure() + image = self.session.image(record.dex) + manager = self._managers.get(record.dex) + if manager is None: + manager = self._factory(memoryview(image.data)) + self._managers[record.dex] = manager + return decompile_class( + image, + record.descriptor, + record.index, + manager=manager, + decompile=self._decompile, + opcodes=self._opcodes, + ) diff --git a/src/tocode/cli.py b/src/tocode/cli.py index 7387856..8f8b546 100644 --- a/src/tocode/cli.py +++ b/src/tocode/cli.py @@ -7,6 +7,9 @@ from pathlib import Path from .analysis import create_analyzer +from .apk import ApkExportOptions, export_apk +from .apk_native import NativeOptions +from .backends.asc import is_apk_input from .errors import ToCodeError from .exporter import export_binary from .naming import default_output_name @@ -68,7 +71,17 @@ def build_parser() -> argparse.ArgumentParser: "--backend", choices=("auto", "ida", "r2", "angr", "binja"), default=default_backend, - help="Decompiler backend: auto prefers IDA, then r2, then angr as a last-resort fallback; binja drives Binary Ninja and is opt-in (default: TOCODE_BACKEND or auto).", + help="Decompiler backend: auto prefers IDA, then r2, then angr as a last-resort fallback; binja drives Binary Ninja and is opt-in (default: TOCODE_BACKEND or auto). For APK input this selects the backend for the native .so libraries (binja is not supported there).", + ) + parser.add_argument( + "--no-native", + action="store_true", + help="APK input: only export the DEX/Android side; native .so libraries are extracted but not decompiled.", + ) + parser.add_argument( + "--no-splits", + action="store_true", + help="APK input: export base.apk alone instead of merging sibling split_*.apk files.", ) parser.add_argument( "--binja-host", @@ -169,6 +182,11 @@ def main(argv: list[str] | None = None) -> int: if args.binja_view is not None and args.all_views: parser.error("--binja-view and --all-views cannot be combined") if args.backend == "binja": + if args.binary is not None and is_apk_input(args.binary): + parser.error( + "--backend binja is not supported for APK input; native libraries " + "use ida, r2, or angr" + ) return _run_binja(args, progress, parser, argv) if args.binary is None: @@ -179,6 +197,8 @@ def main(argv: list[str] | None = None) -> int: args.out_dir = ( args.out_dir.expanduser().resolve() if args.out_dir is not None else None ) + if is_apk_input(binary): + return _run_apk(binary, args=args, progress=progress, parser=parser) log_root = ( args.out_dir if args.out_dir is not None @@ -208,6 +228,55 @@ def main(argv: list[str] | None = None) -> int: return 0 +def _run_apk( + binary: Path, + *, + args: argparse.Namespace, + progress: Progress, + parser: argparse.ArgumentParser, +) -> int: + if not binary.is_file(): + parser.error(f"input must be a regular file: {binary}") + options = ApkExportOptions( + out_dir=args.out_dir, + jobs=args.jobs, + native=not args.no_native, + splits=not args.no_splits, + native_options=NativeOptions( + backend=args.backend, + analysis_command=args.analysis, + idadir=args.idadir, + ida_domain_path=args.ida_domain_path, + purge_cache=args.purge_cache, + jobs=args.jobs, + tree=args.tree, + entropy=args.entropy, + ), + ) + started = time.monotonic() + try: + summary = export_apk(binary, options=options, progress=progress) + except KeyboardInterrupt: + progress.log("tocode: interrupted") + print("tocode: interrupted", file=sys.stderr) + return 130 + except ToCodeError as exc: + progress.log(f"tocode: {exc}") + print(f"tocode: {exc}", file=sys.stderr) + return 1 + if not args.quiet: + print(f"Project: {summary.root_dir}") + print( + f"Summary: package={summary.package} classes={summary.class_count} " + f"methods={summary.method_count} failures={len(summary.failed_classes)} " + f"natives={summary.native_done}/{summary.native_total}" + ) + for line in summary.native_errors: + print(f"native: {line}", file=sys.stderr) + print(f"Exported in {_format_duration(time.monotonic() - started)}") + return 0 + + def _format_duration(seconds: float) -> str: if seconds >= 60: minutes, secs = divmod(int(round(seconds)), 60) diff --git a/src/tocode/exporter.py b/src/tocode/exporter.py index 5c67f25..646dbbb 100644 --- a/src/tocode/exporter.py +++ b/src/tocode/exporter.py @@ -532,6 +532,9 @@ class ExportContext: tree_enabled: bool entropy_enabled: bool = False restart: bool = False + # Free-form provenance shown at the top of the generated AGENTS.md (e.g. a + # native library extracted from an APK export). + origin: str | None = None analysis: ProgramAnalysis | None = None root: Path | None = None raw_dir: Path | None = None @@ -592,6 +595,7 @@ def export_binary( tree: bool = False, entropy: bool = False, restart: bool = False, + origin: str | None = None, ) -> ExportSummary: progress = progress or analyzer.progress context = ExportContext( @@ -602,6 +606,7 @@ def export_binary( tree_enabled=tree, entropy_enabled=entropy, restart=restart, + origin=origin, ) try: _prepare_tree(context) @@ -2514,7 +2519,10 @@ def write_docs() -> None: write_text_atomic( root / "AGENTS.md", build_export_agents( - analysis, context.header_name, tree_enabled=context.tree_enabled + analysis, + context.header_name, + tree_enabled=context.tree_enabled, + origin=context.origin, ), ) write_text_atomic(root / "CLAUDE.md", "@./AGENTS.md\n") @@ -2897,7 +2905,11 @@ def write_manifest(context: ExportContext) -> Path: def build_export_agents( - analysis: ProgramAnalysis, header_name: str, *, tree_enabled: bool = True + analysis: ProgramAnalysis, + header_name: str, + *, + tree_enabled: bool = True, + origin: str | None = None, ) -> str: section_files = sorted( [ @@ -2916,6 +2928,7 @@ def build_export_agents( "You are working inside a ToCode binary export.", "Treat the recovered source, assembly, metadata, and raw section data as evidence for reverse engineering.", "", + *(["## Origin", "", origin, ""] if origin else []), "## Mission", "", "Reverse the binary, answer user questions, or serve as an oracle/helper to the user regarding this recovered source code.", diff --git a/src/tocode/schema.py b/src/tocode/schema.py index ddf1751..62a8c73 100644 --- a/src/tocode/schema.py +++ b/src/tocode/schema.py @@ -263,3 +263,19 @@ class ExportSummary: tree_function_index_path: Path | None = None manifest_path: Path | None = None data_dir: Path | None = None + + +@dataclass(slots=True) +class ApkExportSummary: + root_dir: Path + package: str + apks: list[Path] + class_count: int + method_count: int + source_files: list[Path] + failed_classes: list[tuple[str, str]] + native_total: int + native_done: int + native_errors: list[str] = field(default_factory=list) + manifest_path: Path | None = None + seconds: float = 0.0 diff --git a/tests/test_apk_export.py b/tests/test_apk_export.py new file mode 100644 index 0000000..1541dfc --- /dev/null +++ b/tests/test_apk_export.py @@ -0,0 +1,989 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import threading +import time +from typing import Any +import zipfile + +import pytest + +from tocode import apk as apk_module +from tocode import apk_metadata as meta +from tocode.apk import ApkExportOptions, assign_method_lines, export_apk +from tocode.apk_native import ( + NativeLib, + NativeRunner, + classify_native_entry, + extract_native_libs, +) +from tocode.backends.asc import ( + AscSession, + ClassRecord, + DecompiledClass, + DexImage, + FieldRecord, + MethodRecord, + StringRecord, +) +from tocode.errors import ToCodeError +from tocode.progress import Progress + + +SAMPLE_MANIFEST = """ + + + + + + + + + + + + + + + + + + + + + + +""" + +SAMPLE_JAVA = """package com.example.app; +public class MainActivity extends android.app.Activity { + private int counter; + + public MainActivity() + { + return; + } + + @Override + protected void onCreate(android.os.Bundle p1) + { + this.counter = 1; + return; + } + + private native int nativeAdd(int p1, int p2); + + public int add(int p1) + { + return p1; + } + + public int add(int p1, int p2) + { + return (p1 + p2); + } +} +""" + + +def _method( + id_: int, + cls: str, + name: str, + params: list[str], + ret: str, + flags: int = 0x1, + code: int = 0, +) -> MethodRecord: + return MethodRecord( + id=id_, + dex="classes.dex", + index=id_, + class_descriptor=cls, + name=name, + return_type=ret, + params=params, + access_flags=flags, + is_direct=False, + code_offset=code, + code_size=8 if code else 0, + registers=len(params) + 2, + ) + + +def _main_activity_record() -> ClassRecord: + cls = "Lcom/example/app/MainActivity;" + return ClassRecord( + descriptor=cls, + dex="classes.dex", + index=0, + access_flags=0x1, + superclass="Landroid/app/Activity;", + interfaces=[], + source_file="MainActivity.java", + methods=[ + _method(0, cls, "", [], "V", 0x10001, code=0x100), + _method(1, cls, "onCreate", ["Landroid/os/Bundle;"], "V", 0x4, code=0x120), + _method(2, cls, "nativeAdd", ["I", "I"], "I", 0x102), + _method(3, cls, "add", ["I"], "I", 0x1, code=0x140), + _method(4, cls, "add", ["I", "I"], "I", 0x1, code=0x160), + ], + fields=[FieldRecord("classes.dex", 0, cls, "counter", "I", 0x2)], + ) + + +def _helper_record() -> ClassRecord: + cls = "Lcom/example/util/Helper;" + return ClassRecord( + descriptor=cls, + dex="classes.dex", + index=1, + access_flags=0x11, + superclass="Ljava/lang/Object;", + interfaces=["Ljava/lang/Runnable;"], + source_file=None, + methods=[_method(5, cls, "run", [], "V", 0x1, code=0x200)], + fields=[], + ) + + +class FakeSession(AscSession): + """Inventory built by hand: no droidasc, no real DEX.""" + + def load(self) -> None: + self.images = [ + DexImage("classes.dex", self.apk_set.primary, "classes.dex", b"dex", None) + ] + self._images_by_name = {} + self.classes = [_main_activity_record(), _helper_record()] + self.methods = [m for record in self.classes for m in record.methods] + self._by_descriptor = {record.descriptor: record for record in self.classes} + self._method_by_descriptor = {m.descriptor: m for m in self.methods} + self.strings = [ + StringRecord("classes.dex", 0, "https://api.example.com/v1", 0x10), + StringRecord("classes.dex", 1, "hello", 0x30), + ] + # onCreate -> add(int); add(int) -> Helper.run; run -> external + self.methods[1].callees = [3] + self.methods[3].callers = [1] + self.methods[3].callees = [5] + self.methods[5].callers = [3] + self.methods[5].external_calls = ["Ljava/lang/Thread;->start()V"] + self.methods[1].string_ref_count = 1 + self.string_xrefs = {("classes.dex", 0): [1]} + + +class FakeDecompiler: + def __init__(self, session: AscSession) -> None: + self.session = session + + def decompile(self, record: ClassRecord) -> DecompiledClass: + if record.descriptor.endswith("Helper;"): + return DecompiledClass(record.descriptor, None, "boom", {}) + return DecompiledClass(record.descriptor, SAMPLE_JAVA, None, {}) + + +def _build_apk(path: Path, *, libs: bool = True) -> Path: + entries: dict[str, bytes] = { + "AndroidManifest.xml": b"\x03\x00\x08\x00binary", + "classes.dex": b"dex\n035\x00" + b"\x00" * 64, + "assets/config.json": b"{}", + "res/values/strings.xml": b"", + "resources.arsc": b"garbage", + "META-INF/MANIFEST.MF": b"Manifest-Version: 1.0", + } + if libs: + entries["lib/arm64-v8a/libfoo.so"] = b"\x7fELF" + b"\x00" * 32 + entries["lib/armeabi-v7a/libfoo.so"] = b"\x7fELF" + b"\x01" * 32 + entries["assets/hidden.so"] = b"\x7fELF" + b"\x02" * 32 + entries["assets/plain.so"] = b"not an elf" + with zipfile.ZipFile(path, "w") as archive: + for name, data in entries.items(): + archive.writestr(name, data) + return path + + +@pytest.fixture +def fake_backend(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(apk_module, "AscSession", FakeSession) + monkeypatch.setattr(apk_module, "manifest_xml", lambda apk: SAMPLE_MANIFEST) + + +def _fake_native_exporter(fail_name: str | None = None) -> Any: + calls: list[tuple[str, str, Path]] = [] + + def run( + lib: NativeLib, out_dir: Path, progress: Progress + ) -> tuple[str, str, int, int]: + calls.append((lib.abi, lib.name, out_dir)) + if fail_name and lib.name == fail_name and lib.abi == "armeabi-v7a": + raise ToCodeError("no decompiler backend available") + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "functions.json").write_text("{}", encoding="utf-8") + progress.log("fake native export") + return ("r2", "radare2 pdc", 12, 1) + + run.calls = calls # type: ignore[attr-defined] + return run + + +# -------------------------------------------------------------------------- +# manifest +# -------------------------------------------------------------------------- + + +def test_parse_manifest_extracts_package_components_and_permissions() -> None: + info = meta.parse_manifest(SAMPLE_MANIFEST, apk_name="base.apk") + + assert info.package == "com.example.app" + assert info.version_code == "48" and info.version_name == "4.5" + assert info.min_sdk == "24" and info.target_sdk == "34" + assert info.permissions == [ + "android.permission.INTERNET", + "android.permission.READ_SMS", + ] + assert info.main_activities == [".MainActivity"] + kinds = [(item.kind, item.name, item.exported) for item in info.components] + assert ".MainActivity" in [k[1] for k in kinds] + exported = {item.name: item.exported for item in info.components} + assert exported[".MainActivity"] is True # intent filter => exported + assert exported["com.example.app.Hidden"] is False + assert exported[".Svc"] is True + assert exported[".Prov"] is None + assert info.application["android:debuggable"] == "true" + + +def test_manifest_json_resolves_relative_component_names() -> None: + info = meta.parse_manifest(SAMPLE_MANIFEST, apk_name="base.apk") + doc = meta.manifest_json(info, [], xml_path=Path("AndroidManifest.xml")) + + assert doc["main_activities"] == ["com.example.app.MainActivity"] + names = {item["name"]: item for item in doc["components"]} + assert names["com.example.app.Boot"]["descriptor"] == "Lcom/example/app/Boot;" + assert names["com.example.app.Svc"]["permission"] == "com.example.P" + assert [item["name"] for item in doc["exported_components"]] == [ + "com.example.app.MainActivity", + "com.example.app.Svc", + "com.example.app.Boot", + ] + assert doc["dangerous_permissions"] == ["android.permission.READ_SMS"] + + +def test_parse_manifest_reports_parse_errors() -> None: + info = meta.parse_manifest(" None: + method = _method(0, "Lcom/ex_ample/Main$Inner;", "do_it", ["I"], "V", 0x100) + assert meta.jni_symbol(method) == "Java_com_ex_1ample_Main_00024Inner_do_1it" + + +# -------------------------------------------------------------------------- +# method line mapping +# -------------------------------------------------------------------------- + + +def test_assign_method_lines_maps_constructors_overloads_and_natives() -> None: + record = _main_activity_record() + assign_method_lines(record, SAMPLE_JAVA) + lines = { + m.name + str(len(m.params)): (m.line_start, m.line_end) for m in record.methods + } + + assert lines["0"] == (5, 8) + assert lines["onCreate1"] == (10, 15) # includes the @Override line + assert lines["nativeAdd2"] == (17, 17) + assert lines["add1"] == (19, 22) + assert lines["add2"] == (24, 27) + + +# -------------------------------------------------------------------------- +# native libs +# -------------------------------------------------------------------------- + + +def test_classify_native_entry_detects_abis_and_elf_magic() -> None: + assert classify_native_entry("lib/arm64-v8a/libx.so", b"\x7fELF") == ( + "arm64-v8a", + "libx.so", + ) + assert classify_native_entry("lib/x86/libx.so", b"") == ("x86", "libx.so") + assert classify_native_entry("assets/arm64-v8a/libhidden.so", b"\x7fELF") == ( + "arm64-v8a", + "libhidden.so", + ) + assert classify_native_entry("assets/hidden.so", b"\x7fELF") == ( + "unknown", + "hidden.so", + ) + assert classify_native_entry("assets/plain.so", b"not") is None + assert classify_native_entry("res/raw/data.bin", b"\x7fELF") == ( + "unknown", + "data.bin", + ) + assert classify_native_entry("assets/config.json", b"{}") is None + + +def test_extract_native_libs_writes_every_abi_once(tmp_path: Path) -> None: + apk = _build_apk(tmp_path / "base.apk") + dup = _build_apk(tmp_path / "split_config.arm64_v8a.apk") + root = tmp_path / "out" + + libs = extract_native_libs([apk, dup], root, sha256=lambda path: "h") + + assert [(lib.abi, lib.name) for lib in libs] == [ + ("arm64-v8a", "libfoo.so"), + ("armeabi-v7a", "libfoo.so"), + ("unknown", "hidden.so"), + ] + assert ( + (root / "lib" / "arm64_v8a" / "libfoo.so").read_bytes().startswith(b"\x7fELF") + ) + assert all(lib.source_apk == "base.apk" for lib in libs) + + +def test_native_runner_isolates_failures(tmp_path: Path) -> None: + libs = [ + NativeLib( + "arm64-v8a", + "libfoo.so", + "lib/arm64-v8a/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "a.so", + ), + NativeLib( + "armeabi-v7a", + "libfoo.so", + "lib/armeabi-v7a/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "b.so", + ), + ] + runner = NativeRunner( + libs=libs, + root=tmp_path, + progress=Progress(enabled=False), + exporter=_fake_native_exporter(fail_name="libfoo.so"), + ) + runner.start() + runner.join() + + assert libs[0].status == "done" + assert libs[0].backend == "r2" and libs[0].function_count == 12 + assert libs[0].export_dir == tmp_path / "native" / "arm64_v8a" / "libfoo" + assert libs[1].status == "failed: no decompiler backend available" + assert runner.errors == [ + "armeabi-v7a/libfoo.so: failed: no decompiler backend available" + ] + + +# -------------------------------------------------------------------------- +# end-to-end export with fakes +# -------------------------------------------------------------------------- + + +def test_export_apk_writes_full_tree(tmp_path: Path, fake_backend: None) -> None: + apk = _build_apk(tmp_path / "base.apk") + exporter = _fake_native_exporter(fail_name="libfoo.so") + + summary = export_apk( + apk, + options=ApkExportOptions(out_dir=tmp_path / "export", jobs=1), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + + root = summary.root_dir + assert root == tmp_path / "export" + assert summary.package == "com.example.app" + assert summary.class_count == 2 and summary.method_count == 6 + assert summary.failed_classes == [("Lcom/example/util/Helper;", "boom")] + assert summary.native_total == 3 and summary.native_done == 2 + + for name in ( + "AndroidManifest.xml", + "manifest.json", + "classes.json", + "functions.json", + "function-index.json", + "strings.json", + "imports.json", + "exports.json", + "sections.json", + "reachable.json", + "triage.json", + "package-graph.json", + "native-libs.json", + "project.json", + "export-manifest.json", + "AGENTS.md", + "CLAUDE.md", + "tocode.log", + "data/resources.json", + ): + assert (root / name).is_file(), name + + java = root / "src" / "raw" / "com" / "example" / "app" / "MainActivity.java" + assert java.is_file() + text = java.read_text(encoding="utf-8") + assert text.startswith("// ToCode: Lcom/example/app/MainActivity; from classes.dex") + assert "protected void onCreate" in text + stub = root / "src" / "raw" / "com" / "example" / "util" / "Helper.java" + assert "decompilation failed: boom" in stub.read_text(encoding="utf-8") + assert "public void run();" in stub.read_text(encoding="utf-8") + + # extracted entries and native libs + assert ( + root / "data" / "apk" / "base" / "assets" / "config.json" + ).read_text() == "{}" + assert not (root / "data" / "apk" / "base" / "classes.dex").exists() + assert (root / "lib" / "arm64_v8a" / "libfoo.so").is_file() + assert (root / "lib" / "armeabi_v7a" / "libfoo.so").is_file() + assert (root / "lib" / "unknown" / "hidden.so").is_file() + assert (root / "native" / "arm64_v8a" / "libfoo" / "functions.json").is_file() + assert (root / "native" / "arm64_v8a" / "libfoo" / "tocode.log").is_file() + assert {(abi, name) for abi, name, _ in exporter.calls} == { + ("arm64-v8a", "libfoo.so"), + ("armeabi-v7a", "libfoo.so"), + ("unknown", "hidden.so"), + } + + libs = json.loads((root / "native-libs.json").read_text(encoding="utf-8")) + assert libs["count"] == 3 and libs["done"] == 2 + statuses = {(item["abi"], item["name"]): item["status"] for item in libs["libs"]} + assert statuses[("arm64-v8a", "libfoo.so")] == "done" + assert statuses[("armeabi-v7a", "libfoo.so")].startswith("failed:") + done = next(item for item in libs["libs"] if item["status"] == "done") + assert done["export_dir"].startswith("native/") + assert done["backend"] == "r2" and done["function_count"] == 12 + + manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8")) + assert manifest["package"] == "com.example.app" + assert manifest["main_activities"] == ["com.example.app.MainActivity"] + + functions = json.loads((root / "functions.json").read_text(encoding="utf-8")) + rows = {row["name"]: row for row in functions["functions"]} + on_create = rows["Lcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V"] + assert on_create["address"] == "classes.dex:0x120" + assert on_create["callees"] == ["classes.dex:0x140"] + assert on_create["string_ref_count"] == 1 + assert on_create["source_file"] == "src/raw/com/example/app/MainActivity.java" + assert on_create["source_line_start"] == 11 and on_create["source_line_end"] == 16 + assert on_create["dead_code"] is False + run = rows["Lcom/example/util/Helper;->run()V"] + assert run["callees_imports"] == ["Ljava/lang/Thread;->start()V"] + assert run["callers"] == ["classes.dex:0x140"] + assert run["source_line_start"] == 1 # failure stub covers the whole file + assert rows["Lcom/example/app/MainActivity;->nativeAdd(II)I"]["native"] is True + + index = json.loads((root / "function-index.json").read_text(encoding="utf-8")) + assert index["schema_version"] == 2 and index["language"] == "java" + entry = next( + item for item in index["functions"] if item["name"].endswith("add(II)I") + ) + assert entry["c"] == { + "path": "src/raw/com/example/app/MainActivity.java", + "line_start": 25, + "line_end": 28, + } + assert entry["asm"] is None + + imports = json.loads((root / "imports.json").read_text(encoding="utf-8")) + assert imports["imports"][0]["dll"] == "java.lang" + assert ( + imports["imports"][0]["functions"][0]["name"] == "Ljava/lang/Thread;->start()V" + ) + + exports = json.loads((root / "exports.json").read_text(encoding="utf-8")) + kinds = [(item["kind"], item["name"]) for item in exports["exports"]] + assert ("activity", "com.example.app.MainActivity") in kinds + jni = next(item for item in exports["exports"] if item["kind"] == "jni") + assert jni["forwarder_target"] == "Java_com_example_app_MainActivity_nativeAdd" + + strings = json.loads((root / "strings.json").read_text(encoding="utf-8")) + url = next(item for item in strings["strings"] if item["value"].startswith("https")) + assert url["xrefs"][0]["address"] == "classes.dex:0x120" + + reachable = json.loads((root / "reachable.json").read_text(encoding="utf-8")) + depths = {item["name"]: item["depth"] for item in reachable["reachable"]} + assert depths["Lcom/example/util/Helper;->run()V"] == 1 + assert reachable["unreachable_count"] == 0 + + triage = json.loads((root / "triage.json").read_text(encoding="utf-8")) + assert triage["binary_type"] == "apk" + assert triage["dangerous_permissions"] == ["android.permission.READ_SMS"] + assert ( + triage["native_methods"][0]["jni_symbol"] + == "Java_com_example_app_MainActivity_nativeAdd" + ) + assert triage["strings_of_interest"][0]["value"] == "https://api.example.com/v1" + assert len(triage["native_libs"]) == 3 + + sections = json.loads((root / "sections.json").read_text(encoding="utf-8")) + kinds_by_name = {item["name"]: item["type"] for item in sections["sections"]} + assert kinds_by_name["classes.dex"] == "dex" + assert kinds_by_name["lib/arm64-v8a/libfoo.so"] == "native" + assert kinds_by_name["assets/config.json"] == "asset" + assert kinds_by_name["resources.arsc"] == "arsc" + + graph = json.loads((root / "package-graph.json").read_text(encoding="utf-8")) + app = next( + item for item in graph["packages"] if item["package"] == "com.example.app" + ) + assert app["calls_packages"] == [{"package": "com.example.util", "calls": 1}] + + project = json.loads((root / "project.json").read_text(encoding="utf-8")) + assert project["backend"] == "asc" and project["package"] == "com.example.app" + assert project["native_enabled"] is True + manifest_doc = json.loads( + (root / "export-manifest.json").read_text(encoding="utf-8") + ) + assert manifest_doc["format"] == "apk" + assert manifest_doc["failures"] == [ + {"address": None, "name": "Lcom/example/util/Helper;", "error": "boom"} + ] + assert manifest_doc["native_errors"] == [ + "armeabi-v7a/libfoo.so: failed: no decompiler backend available" + ] + agents = (root / "AGENTS.md").read_text(encoding="utf-8") + assert "com.example.app" in agents and "native///" in agents + assert (root / "CLAUDE.md").read_text(encoding="utf-8") == "@./AGENTS.md\n" + log = (root / "tocode.log").read_text(encoding="utf-8") + assert "Native decompilation finished: 2/3" in log + assert "export Lcom/example/util/Helper; failed: boom" in log + + +def test_export_apk_no_native_only_extracts_libs( + tmp_path: Path, fake_backend: None +) -> None: + apk = _build_apk(tmp_path / "base.apk") + exporter = _fake_native_exporter() + + summary = export_apk( + apk, + options=ApkExportOptions(out_dir=tmp_path / "export", jobs=1, native=False), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + + assert exporter.calls == [] + assert summary.native_done == 0 and summary.native_total == 3 + root = summary.root_dir + assert (root / "lib" / "arm64_v8a" / "libfoo.so").is_file() + assert not (root / "native").exists() + libs = json.loads((root / "native-libs.json").read_text(encoding="utf-8")) + assert {item["status"] for item in libs["libs"]} == {"skipped: --no-native"} + assert "--no-native" in (root / "AGENTS.md").read_text(encoding="utf-8") + project = json.loads((root / "project.json").read_text(encoding="utf-8")) + assert project["native_enabled"] is False + + +def test_export_apk_default_out_dir_uses_package_name( + tmp_path: Path, fake_backend: None, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.delenv("TOCODE_DEFAULT_OUT_ROOT", raising=False) + apk = _build_apk(tmp_path / "base.apk", libs=False) + + summary = export_apk( + apk, + options=ApkExportOptions(jobs=1), + progress=Progress(enabled=False), + decompiler_factory=FakeDecompiler, + ) + + assert summary.root_dir == tmp_path / "com_example_app_decompiler" + assert summary.native_total == 0 + + +def test_export_apk_merges_sibling_splits_and_bundles( + tmp_path: Path, fake_backend: None +) -> None: + _build_apk(tmp_path / "base.apk", libs=False) + with zipfile.ZipFile(tmp_path / "split_config.arm64_v8a.apk", "w") as archive: + archive.writestr("AndroidManifest.xml", b"\x03\x00\x08\x00") + archive.writestr("lib/arm64-v8a/libsplit.so", b"\x7fELF" + b"\x00" * 8) + with zipfile.ZipFile(tmp_path / "app.apks", "w") as bundle: + bundle.write(tmp_path / "base.apk", "base.apk") + bundle.write( + tmp_path / "split_config.arm64_v8a.apk", "split_config.arm64_v8a.apk" + ) + + exporter = _fake_native_exporter() + merged = export_apk( + tmp_path / "base.apk", + options=ApkExportOptions(out_dir=tmp_path / "merged", jobs=1), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + assert [item.name for item in merged.apks] == [ + "base.apk", + "split_config.arm64_v8a.apk", + ] + assert merged.native_total == 1 and merged.native_done == 1 + manifest = json.loads( + (merged.root_dir / "manifest.json").read_text(encoding="utf-8") + ) + assert [item["apk"] for item in manifest["splits"]] == [ + "split_config.arm64_v8a.apk" + ] + assert ( + merged.root_dir + / "data" + / "apk" + / "split_config_arm64_v8a" + / "AndroidManifest.xml" + ).is_file() + + alone = export_apk( + tmp_path / "base.apk", + options=ApkExportOptions(out_dir=tmp_path / "alone", jobs=1, splits=False), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + assert [item.name for item in alone.apks] == ["base.apk"] + assert alone.native_total == 0 + + bundled = export_apk( + tmp_path / "app.apks", + options=ApkExportOptions(out_dir=tmp_path / "bundled", jobs=1), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + assert [item.name for item in bundled.apks] == [ + "base.apk", + "split_config.arm64_v8a.apk", + ] + assert bundled.native_total == 1 + assert not any(item.exists() for item in bundled.apks) # bundle workdir cleaned + + +def test_export_apk_rejects_missing_or_invalid_input(tmp_path: Path) -> None: + with pytest.raises(ToCodeError): + export_apk(tmp_path / "missing.apk", progress=Progress(enabled=False)) + bad = tmp_path / "bad.apk" + bad.write_bytes(b"nope") + with pytest.raises(ToCodeError): + export_apk(bad, progress=Progress(enabled=False)) + + +def test_export_apk_removes_stale_java_files( + tmp_path: Path, fake_backend: None +) -> None: + apk = _build_apk(tmp_path / "base.apk", libs=False) + stale = tmp_path / "export" / "src" / "raw" / "old" / "Gone.java" + stale.parent.mkdir(parents=True) + stale.write_text("stale", encoding="utf-8") + + export_apk( + apk, + options=ApkExportOptions(out_dir=tmp_path / "export", jobs=1), + progress=Progress(enabled=False), + decompiler_factory=FakeDecompiler, + ) + + assert not stale.exists() + + +@pytest.mark.skipif( + not os.environ.get("TOCODE_TEST_APK"), reason="set TOCODE_TEST_APK to a real APK" +) +def test_export_real_apk_with_asc(tmp_path: Path) -> None: + pytest.importorskip("droidasc") + apk = Path(os.environ["TOCODE_TEST_APK"]) + + summary = export_apk( + apk, + options=ApkExportOptions( + out_dir=tmp_path / "export", native=False, splits=False + ), + progress=Progress(enabled=False), + ) + + assert summary.class_count > 0 + classes = json.loads( + (summary.root_dir / "classes.json").read_text(encoding="utf-8") + ) + assert classes["count"] == summary.class_count + assert ( + len(list((summary.root_dir / "src" / "raw").rglob("*.java"))) + == summary.class_count + ) + + +class _FakeProcess: + def __init__(self, exitcode: int) -> None: + self.exitcode = exitcode + self.joined = False + self.pid: int | None = None + + def is_alive(self) -> bool: + return False + + def join(self) -> None: + self.joined = True + + +class _FakeQueue: + def __init__(self, item: Any = None) -> None: + self.item = item + + def get(self, timeout: float | None = None) -> Any: + import queue + + if self.item is None: + raise queue.Empty + return self.item + + +def test_collect_child_result_reports_a_killed_export( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from tocode import apk_native + from tocode.apk_native import collect_child_result + + killed: list[int | None] = [] + monkeypatch.setattr(apk_native, "kill_process_group", killed.append) + process = _FakeProcess(-9) + process.pid = 4242 + with pytest.raises(ToCodeError) as info: + collect_child_result(process, _FakeQueue(), poll_seconds=0.01) + + assert "killed by signal 9" in str(info.value) + assert "--no-native" in str(info.value) + assert process.joined + assert killed == [4242] # the child's leftover worker pool is reaped too + + +def test_collect_child_result_passes_through_errors_and_results() -> None: + from tocode.apk_native import collect_child_result + + with pytest.raises(ToCodeError, match="no decompiler backend"): + collect_child_result( + _FakeProcess(1), _FakeQueue(("error", "no decompiler backend available")) + ) + with pytest.raises(ToCodeError, match="exited with code 3"): + collect_child_result(_FakeProcess(3), _FakeQueue(), poll_seconds=0.01) + + assert collect_child_result( + _FakeProcess(0), _FakeQueue(("ok", ("r2", "radare2 pdc", 12, 1))) + ) == ("r2", "radare2 pdc", 12, 1) + + +def test_native_runner_waits_for_free_memory(tmp_path: Path) -> None: + libs = [ + NativeLib( + "arm64-v8a", + "libfoo.so", + "lib/arm64-v8a/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "a.so", + ) + ] + readings = iter([100, 200, 4096]) + seen: list[int | None] = [] + + def available() -> int | None: + value = next(readings, 4096) + seen.append(value) + return value + + runner = NativeRunner( + libs=libs, + root=tmp_path, + progress=Progress(enabled=False), + exporter=_fake_native_exporter(), + min_free_mb=1024, + available_memory=available, + poll_seconds=0.01, + ) + runner.start() + runner.join() + + assert seen == [100, 200, 4096] # waited until there was headroom + assert libs[0].status == "done" + + +def test_native_runner_memory_gate_is_off_without_a_floor(tmp_path: Path) -> None: + libs = [ + NativeLib( + "x86", + "libfoo.so", + "lib/x86/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "a.so", + ) + ] + calls = 0 + + def available() -> int | None: + nonlocal calls + calls += 1 + return 0 + + runner = NativeRunner( + libs=libs, + root=tmp_path, + progress=Progress(enabled=False), + exporter=_fake_native_exporter(), + available_memory=available, + ) + runner.start() + runner.join() + + assert calls == 0 and libs[0].status == "done" + + +def test_nested_log_tail_mirrors_lines_and_counts_functions(tmp_path: Path) -> None: + from tocode.apk_native import NestedLogTail + + main = Progress(enabled=False) + main.set_log_path(tmp_path / "main.log") + nested = tmp_path / "nested.log" + nested.write_text( + "2026-09-19T20:41:14 Analyzing with IDA Domain auto-analysis\n", + encoding="utf-8", + ) + lib = NativeLib( + "arm64-v8a", + "libfoo.so", + "lib/arm64-v8a/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "libfoo.so", + ) + tail = NestedLogTail(nested, lib, main, poll_seconds=0.01) + tail.start() + with nested.open("a", encoding="utf-8") as handle: + handle.write( + "2026-09-19T20:41:15 Rendering and writing 3 functions in 1 clusters\n" + ) + handle.write("2026-09-19T20:41:15 export sub_1000 0x1000 - 10 bytes done\n") + handle.write("2026-09-19T20:41:15 export sub_2000 0x2000 - 10 bytes done\n") + handle.write("2026-09-19T20:41:15 export sub_3000 0x3000 - 10 bytes\n") # start + handle.write("plain backend stderr line\n") + handle.write("2026-09-19T20:41:16 partial line without newline") + tail.stop() + + assert tail.total == 3 and tail.rendered == 2 + assert tail.status() == "2/3 functions" + mirrored = (tmp_path / "main.log").read_text(encoding="utf-8") + assert "native[libfoo.so]: Analyzing with IDA Domain auto-analysis" in mirrored + assert "native[libfoo.so]: Rendering and writing 3 functions" in mirrored + assert "native[libfoo.so]: plain backend stderr line" in mirrored + assert "export sub_1000" not in mirrored # per-function lines feed the bar only + assert ( + "export sub_3000" not in mirrored + ) # start lines are neither counted nor shown + assert "partial line" not in mirrored + + +def test_native_runner_describe_current_reports_progress(tmp_path: Path) -> None: + from tocode import apk_native + + lib = NativeLib( + "arm64-v8a", + "libfoo.so", + "lib/arm64-v8a/libfoo.so", + "base.apk", + 1, + "h", + tmp_path / "a.so", + ) + started = threading.Event() + release = threading.Event() + + def slow( + lib_: NativeLib, out_dir: Path, progress: Progress + ) -> tuple[str, str, int, int]: + progress.log("Rendering and writing 5 functions in 1 clusters") + started.set() + release.wait(5) + return ("ida", "Hex-Rays", 5, 0) + + runner = apk_native.NativeRunner( + libs=[lib], root=tmp_path, progress=Progress(enabled=False), exporter=slow + ) + assert runner.describe_current() is None + runner.start() + assert started.wait(5) + deadline = time.monotonic() + 5 + while runner.current_tail is not None and runner.current_tail.total is None: + if time.monotonic() > deadline: + break + time.sleep(0.01) + status = runner.describe_current() + release.set() + runner.join() + + assert status is not None + assert status.startswith("native: libfoo.so (arm64-v8a) 0/5 functions") + assert "0/1 libraries finished" in status + assert runner.describe_current() is None + + +def test_describe_origin_names_the_apk_package(tmp_path: Path) -> None: + from tocode.apk_native import describe_origin + + lib = NativeLib( + "arm64-v8a", + "libfoo.so", + "lib/arm64-v8a/libfoo.so", + "base.apk", + 1, + "abc", + tmp_path / "libfoo.so", + package="com.example.app", + ) + + text = describe_origin(lib) + + assert text.startswith( + "This is a decompiled native library from the `com.example.app` APK: " + "`libfoo.so` (arm64-v8a, `lib/arm64-v8a/libfoo.so` in `base.apk`, sha256 `abc`)." + ) + assert "Java___" in text + assert "an Android app" in describe_origin( + NativeLib("x86", "l.so", "lib/x86/l.so", "a.apk", 1, "h", tmp_path / "l.so") + ) + + +def test_export_apk_tags_native_libs_with_the_package( + tmp_path: Path, fake_backend: None +) -> None: + apk = _build_apk(tmp_path / "base.apk") + seen: list[str] = [] + + def exporter( + lib: NativeLib, out_dir: Path, progress: Progress + ) -> tuple[str, str, int, int]: + seen.append(lib.package) + return ("r2", "radare2 pdc", 1, 0) + + export_apk( + apk, + options=ApkExportOptions(out_dir=tmp_path / "export", jobs=1), + progress=Progress(enabled=False), + native_exporter=exporter, + decompiler_factory=FakeDecompiler, + ) + + assert seen == ["com.example.app"] * 3 diff --git a/tests/test_asc_backend.py b/tests/test_asc_backend.py new file mode 100644 index 0000000..fcbce7e --- /dev/null +++ b/tests/test_asc_backend.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +from pathlib import Path +import zipfile + +import pytest + +from tocode.backends.asc import ( + ApkSet, + class_java_relpath, + discover_apk_set, + is_apk_bundle, + is_apk_input, + java_type, + method_descriptor, + scan_bytecode_refs, +) +from tocode.errors import ToCodeError + + +def _write_zip(path: Path, entries: dict[str, bytes]) -> Path: + with zipfile.ZipFile(path, "w") as archive: + for name, data in entries.items(): + archive.writestr(name, data) + return path + + +def test_java_type_converts_descriptors() -> None: + assert java_type("Lcom/foo/Bar$Baz;") == "com.foo.Bar$Baz" + assert java_type("[I") == "int[]" + assert java_type("[[Ljava/lang/String;") == "java.lang.String[][]" + assert java_type("V") == "void" + + +def test_method_descriptor_round_trips_signature() -> None: + assert ( + method_descriptor("Lcom/foo/Bar;", "run", ["I", "Ljava/lang/String;"], "V") + == "Lcom/foo/Bar;->run(ILjava/lang/String;)V" + ) + + +def test_class_java_relpath_uses_package_folders() -> None: + assert class_java_relpath("Lcom/foo/Bar;") == Path("com/foo/Bar.java") + assert class_java_relpath("Lcom/foo/Bar$Inner;") == Path("com/foo/Bar$Inner.java") + assert class_java_relpath("LNoPackage;") == Path("NoPackage.java") + assert class_java_relpath("La/b/c/ünïcode;") == Path("a/b/c/n_code.java") + + +def test_apk_input_detection() -> None: + assert is_apk_input(Path("app.APK")) + assert is_apk_input(Path("bundle.apks")) + assert is_apk_input(Path("bundle.xapk")) + assert is_apk_bundle(Path("bundle.apks")) + assert not is_apk_bundle(Path("app.apk")) + assert not is_apk_input(Path("libfoo.so")) + + +def test_discover_apk_set_picks_up_sibling_splits(tmp_path: Path) -> None: + base = _write_zip(tmp_path / "base.apk", {"AndroidManifest.xml": b"x"}) + _write_zip(tmp_path / "split_config.en.apk", {"AndroidManifest.xml": b"x"}) + _write_zip(tmp_path / "split_config.arm64_v8a.apk", {"AndroidManifest.xml": b"x"}) + (tmp_path / "split_broken.apk").write_bytes(b"not a zip") + _write_zip(tmp_path / "other.apk", {"AndroidManifest.xml": b"x"}) + + apk_set = discover_apk_set(base, workdir=tmp_path / "work") + + assert apk_set.primary == base.resolve() + assert [item.name for item in apk_set.apks] == [ + "base.apk", + "split_config.arm64_v8a.apk", + "split_config.en.apk", + ] + assert apk_set.workdir is None + assert apk_set.is_split + + +def test_discover_apk_set_can_ignore_splits(tmp_path: Path) -> None: + base = _write_zip(tmp_path / "base.apk", {"AndroidManifest.xml": b"x"}) + _write_zip(tmp_path / "split_config.en.apk", {"AndroidManifest.xml": b"x"}) + + apk_set = discover_apk_set(base, splits=False, workdir=tmp_path / "work") + + assert apk_set.apks == [base.resolve()] + assert not apk_set.is_split + + +def test_discover_apk_set_exports_standalone_split_alone(tmp_path: Path) -> None: + _write_zip(tmp_path / "base.apk", {"AndroidManifest.xml": b"x"}) + split = _write_zip(tmp_path / "split_config.en.apk", {"AndroidManifest.xml": b"x"}) + + apk_set = discover_apk_set(split, workdir=tmp_path / "work") + + assert apk_set.apks == [split.resolve()] + + +def test_discover_apk_set_unpacks_bundles(tmp_path: Path) -> None: + inner_base = _write_zip(tmp_path / "inner_base.apk", {"AndroidManifest.xml": b"b"}) + inner_split = _write_zip( + tmp_path / "inner_split.apk", {"AndroidManifest.xml": b"s"} + ) + bundle = _write_zip( + tmp_path / "app.apks", + { + "split_config.en.apk": inner_split.read_bytes(), + "base.apk": inner_base.read_bytes(), + "nested/ignored.apk": inner_split.read_bytes(), + "toc.pb": b"meta", + }, + ) + workdir = tmp_path / "work" + + apk_set = discover_apk_set(bundle, workdir=workdir) + + assert apk_set.workdir == workdir + assert [item.name for item in apk_set.apks] == ["base.apk", "split_config.en.apk"] + assert apk_set.primary == workdir / "base.apk" + assert all(item.parent == workdir for item in apk_set.apks) + + +def test_discover_apk_set_rejects_non_zip_and_empty_bundle(tmp_path: Path) -> None: + bad = tmp_path / "bad.apk" + bad.write_bytes(b"nope") + with pytest.raises(ToCodeError): + discover_apk_set(bad, workdir=tmp_path / "work") + empty = _write_zip(tmp_path / "empty.apks", {"toc.pb": b""}) + with pytest.raises(ToCodeError): + discover_apk_set(empty, workdir=tmp_path / "work2") + + +def test_apk_set_is_picklable_for_spawned_workers(tmp_path: Path) -> None: + import pickle + + apk_set = ApkSet(primary=tmp_path / "base.apk", apks=[tmp_path / "base.apk"]) + assert pickle.loads(pickle.dumps(apk_set)) == apk_set + + +def test_scan_bytecode_refs_reads_constant_pool_indices() -> None: + pytest.importorskip("droidasc") + bytecode = bytes.fromhex( + "1a000500" # const-string v0, string@5 + "6e10020100 00" # invoke-virtual {v0}, method@0x102 + "62000300" # sget-object v0, field@3 + "1c000700" # const-class v0, type@7 + "1b0078563412" # const-string/jumbo v0, string@0x12345678 + "0e00" # return-void + ) + + refs = scan_bytecode_refs(bytecode) + + assert refs == [ + ("string", 5), + ("method", 0x102), + ("field", 3), + ("type", 7), + ("string", 0x12345678), + ] + + +def test_scan_bytecode_refs_stops_at_switch_payload() -> None: + pytest.importorskip("droidasc") + bytecode = bytes.fromhex( + "2b0003000000" # packed-switch v0, +3 (payload starts at byte 6) + "00011a000900" # payload bytes: must not be decoded as const-string + ) + + assert scan_bytecode_refs(bytecode) == [] diff --git a/tests/test_cli.py b/tests/test_cli.py index 8cd3d41..76e5d48 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,3 +40,58 @@ def test_parser_uses_entropy_as_opt_in_flag() -> None: assert parser.parse_args(["sample.bin"]).entropy is False assert parser.parse_args(["--entropy", "sample.bin"]).entropy is True + + +def test_parser_has_apk_flags() -> None: + parser = build_parser() + + default_args = parser.parse_args(["app.apk"]) + apk_args = parser.parse_args(["--no-native", "--no-splits", "app.apk"]) + + assert default_args.no_native is False and default_args.no_splits is False + assert apk_args.no_native is True and apk_args.no_splits is True + + +def test_main_rejects_binja_backend_for_apk_input(tmp_path) -> None: + from tocode.cli import main + + apk = tmp_path / "app.apk" + apk.write_bytes(b"PK\x05\x06" + b"\x00" * 18) + + with pytest.raises(SystemExit) as info: + main(["--backend", "binja", str(apk)]) + + assert info.value.code == 2 + + +def test_main_routes_apk_input_to_apk_export(tmp_path, monkeypatch) -> None: + from types import SimpleNamespace + + from tocode import cli + + apk = tmp_path / "app.apk" + apk.write_bytes(b"PK\x05\x06" + b"\x00" * 18) + seen = {} + + def fake_export(binary, *, options, progress): + seen["binary"] = binary + seen["options"] = options + return SimpleNamespace( + root_dir=tmp_path / "out", + package="com.example", + class_count=1, + method_count=2, + failed_classes=[], + native_total=1, + native_done=1, + native_errors=[], + ) + + monkeypatch.setattr(cli, "export_apk", fake_export) + + assert cli.main(["--no-native", "-j", "3", "--backend", "r2", "-q", str(apk)]) == 0 + assert seen["binary"] == apk.resolve() + assert seen["options"].native is False + assert seen["options"].jobs == 3 + assert seen["options"].native_options.backend == "r2" + assert seen["options"].native_options.jobs == 3 diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 09bcb34..da05dd9 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -1258,3 +1258,16 @@ def test_tree_safe_function_preserves_scanner_calls() -> None: assert "strcpy(dest, a1);" in tree_source assert "strcpy__GLIBC_2_17" not in tree_source assert "return 0LL;" in tree_source + + +def test_build_export_agents_includes_origin_section() -> None: + from tocode.exporter import build_export_agents + + analysis = _single_routine_analysis() + with_origin = build_export_agents( + analysis, "sample.h", tree_enabled=False, origin="From the `com.x` APK." + ) + without = build_export_agents(analysis, "sample.h", tree_enabled=False) + + assert "## Origin\n\nFrom the `com.x` APK.\n\n## Mission" in with_origin + assert "## Origin" not in without diff --git a/uv.lock b/uv.lock index 9683cc5..c9e8fcb 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,48 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "alembic" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" }, +] + +[[package]] +name = "androguard" +version = "4.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apkinspector" }, + { name = "asn1crypto" }, + { name = "click" }, + { name = "colorama" }, + { name = "cryptography" }, + { name = "dataset" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "loguru" }, + { name = "lxml" }, + { name = "mutf8" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydot" }, + { name = "pygments" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/a4/c6a1bcc4f4b40098259202f7155214f2ec315eb3ac5923f093646cf352c6/androguard-4.1.4.tar.gz", hash = "sha256:1e117ee4574366a2d7376b8c858433ad724b0a29e4036d9f1a9fda4372180267", size = 956315, upload-time = "2026-06-01T08:58:44.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/fc/dc0df02bcfb4c5182e2c15a60b2cd823f11470ddce8abcf3f99a43394434/androguard-4.1.4-py3-none-any.whl", hash = "sha256:6265a6d4007401cf5a62a98fa99a1c2994644d4311da031a5398efe3bcaa63b0", size = 1026935, upload-time = "2026-06-01T08:58:42.638Z" }, +] + [[package]] name = "angr" version = "9.2.213" @@ -16,30 +58,30 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "archinfo", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "cachetools", marker = "python_full_version < '3.12'" }, - { name = "capstone", marker = "python_full_version < '3.12'" }, - { name = "cffi", marker = "python_full_version < '3.12'" }, - { name = "claripy", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "cle", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "cxxheaderparser", marker = "python_full_version < '3.12'" }, - { name = "gitpython", marker = "python_full_version < '3.12'" }, - { name = "lmdb", marker = "python_full_version < '3.12'" }, - { name = "msgspec", marker = "python_full_version < '3.12' and implementation_name == 'cpython'" }, - { name = "mulpyplexer", marker = "python_full_version < '3.12'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "archinfo", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "cachetools" }, + { name = "capstone" }, + { name = "cffi" }, + { name = "claripy", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "cle", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "cxxheaderparser" }, + { name = "gitpython" }, + { name = "lmdb" }, + { name = "msgspec", marker = "implementation_name == 'cpython'" }, + { name = "mulpyplexer" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "protobuf", marker = "python_full_version < '3.12'" }, - { name = "psutil", marker = "python_full_version < '3.12'" }, - { name = "pycparser", marker = "python_full_version < '3.12'" }, - { name = "pydemumble", marker = "python_full_version < '3.12'" }, - { name = "pypcode", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "pyvex", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "rich", marker = "python_full_version < '3.12'" }, - { name = "sortedcontainers", marker = "python_full_version < '3.12'" }, - { name = "sympy", marker = "python_full_version < '3.12'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pycparser" }, + { name = "pydemumble" }, + { name = "pypcode", version = "3.3.3", source = { registry = "https://pypi.org/simple" } }, + { name = "pyvex", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "rich" }, + { name = "sortedcontainers" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/cc/fc2c05157b0d7d65b919f33ceba47a3e324a4bc965bf9fea0283c4b2de93/angr-9.2.213.tar.gz", hash = "sha256:773ca0912107f64332ae0d98676300c03d3df79b7d507bbb223c65d076505c93", size = 4163352, upload-time = "2026-04-28T18:04:51.768Z" } wheels = [ @@ -73,31 +115,31 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "archinfo", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "cachetools", marker = "python_full_version >= '3.12'" }, - { name = "capstone", marker = "python_full_version >= '3.12'" }, - { name = "cffi", marker = "python_full_version >= '3.12'" }, - { name = "claripy", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "cle", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "cxxheaderparser", marker = "python_full_version >= '3.12'" }, - { name = "gitpython", marker = "python_full_version >= '3.12'" }, - { name = "lmdb", marker = "python_full_version >= '3.12'" }, - { name = "msgspec", marker = "python_full_version >= '3.12' and implementation_name == 'cpython'" }, - { name = "mulpyplexer", marker = "python_full_version >= '3.12'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "platformdirs", marker = "python_full_version >= '3.12'" }, - { name = "protobuf", marker = "python_full_version >= '3.12'" }, - { name = "psutil", marker = "python_full_version >= '3.12'" }, - { name = "pycparser", marker = "python_full_version >= '3.12'" }, - { name = "pydemumble", marker = "python_full_version >= '3.12'" }, - { name = "pypcode", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pyvex", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "rich", marker = "python_full_version >= '3.12'" }, - { name = "rust-demangler", marker = "python_full_version >= '3.12'" }, - { name = "sortedcontainers", marker = "python_full_version >= '3.12'" }, - { name = "sympy", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "archinfo", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "cachetools" }, + { name = "capstone" }, + { name = "cffi" }, + { name = "claripy", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "cle", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "cxxheaderparser" }, + { name = "gitpython" }, + { name = "lmdb" }, + { name = "msgspec", marker = "implementation_name == 'cpython'" }, + { name = "mulpyplexer" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pycparser" }, + { name = "pydemumble" }, + { name = "pypcode", version = "4.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "pyvex", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "rich" }, + { name = "rust-demangler" }, + { name = "sortedcontainers" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/94/d252480aace97c3806448a5f6b37945425fb19c1c8b0571e98b907d58bc7/angr-9.2.221.tar.gz", hash = "sha256:6bde1600b32eb91de556b1729ca5c5c2f7cf63f936afbdea8cafc047858894fd", size = 15051078, upload-time = "2026-06-03T23:38:37.846Z" } wheels = [ @@ -117,6 +159,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/e9/5280f28664e1ae56206a9901e53cd4c296559ceee48b6b17888e8bf20140/angr-9.2.221-cp314-cp314-win_amd64.whl", hash = "sha256:35dbccaa244840194b11af1e80f9ded198903595dd9bf61a97ed25d82fc6a002", size = 18833522, upload-time = "2026-06-03T23:37:47.038Z" }, ] +[[package]] +name = "apkinspector" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/8d/dd382dd4fd58799634d66d8f6eafe2c951a35971aacad6f6f3df6f111383/apkinspector-1.3.7.tar.gz", hash = "sha256:ea5a62f9c6a3127a6191901fa0cc32e23b05caf2034d7062193bd4c0539c7888", size = 28853, upload-time = "2026-08-28T07:11:46.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7f/e958755a08d9331428990464ee86980d43c24b330c4ab59c2fadb125e892/apkinspector-1.3.7-py3-none-any.whl", hash = "sha256:fa3242baa7ba8e0eecd20a2fcf427fcfd34f4043902542d12110c7297649c590", size = 31163, upload-time = "2026-08-28T07:11:45.835Z" }, +] + [[package]] name = "archinfo" version = "9.2.213" @@ -126,7 +177,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "backports-strenum", marker = "python_full_version < '3.11'" }, + { name = "backports-strenum", marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/d9/1773ff1b73db291dcf5ec74bd3280fe28af14a12ac5cf5c95d5b4231b33c/archinfo-9.2.213.tar.gz", hash = "sha256:aa575be5955346ea2e4804acad25a3e2f4fe45275d847c2b102dbe84d2fb9a40", size = 41289, upload-time = "2026-04-28T18:04:55.59Z" } wheels = [ @@ -151,6 +202,24 @@ version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d2/15/09fbfc0d43b66a3845098377249539aa44edcd541e17eabe2e11b4e3855e/arpy-1.1.1.tar.gz", hash = "sha256:3ec36309d2234648ef8dcd2118fe7d81c30195087e0353473546583f3434e776", size = 6774, upload-time = "2013-06-30T21:31:27.411Z" } +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, +] + [[package]] name = "backports-strenum" version = "1.3.1" @@ -388,9 +457,9 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "cachetools", marker = "python_full_version < '3.12'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, - { name = "z3-solver", marker = "python_full_version < '3.12'" }, + { name = "cachetools" }, + { name = "typing-extensions" }, + { name = "z3-solver" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4f/fc/1995734fd0ac68503c7aba645555454f7b2aa94bc995f9c36eba0ab9d346/claripy-9.2.213.tar.gz", hash = "sha256:2205ab333f26d03271951584e6bfd8c9bd4247e9a93f27ba4078ba745baabf2f", size = 147567, upload-time = "2026-04-28T18:04:56.55Z" } wheels = [ @@ -405,8 +474,8 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "cachetools", marker = "python_full_version >= '3.12'" }, - { name = "z3-solver", marker = "python_full_version >= '3.12'" }, + { name = "cachetools" }, + { name = "z3-solver" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/a6/4c83471b5f442c26f3b4c98d4ab4eb612173cbdc4edb6be54bc574440b29/claripy-9.2.221.tar.gz", hash = "sha256:7b11a9aca9b07f8ea01f746f725f3148f2a9548efc91dc51e181da885f37650f", size = 147340, upload-time = "2026-06-03T23:38:46.788Z" } wheels = [ @@ -422,17 +491,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "archinfo", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "arpy", marker = "python_full_version < '3.12'" }, - { name = "cart", marker = "python_full_version < '3.12'" }, - { name = "minidump", marker = "python_full_version < '3.12'" }, - { name = "pefile", marker = "python_full_version < '3.12'" }, - { name = "pyelftools", marker = "python_full_version < '3.12'" }, - { name = "pyvex", version = "9.2.213", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "pyxbe", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "pyxdia", marker = "python_full_version < '3.12'" }, - { name = "sortedcontainers", marker = "python_full_version < '3.12'" }, - { name = "uefi-firmware", marker = "python_full_version < '3.12'" }, + { name = "archinfo", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "arpy" }, + { name = "cart" }, + { name = "minidump" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "pyvex", version = "9.2.213", source = { registry = "https://pypi.org/simple" } }, + { name = "pyxbe", version = "1.0.3", source = { registry = "https://pypi.org/simple" } }, + { name = "pyxdia" }, + { name = "sortedcontainers" }, + { name = "uefi-firmware" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/55/cec8283b3ea283468d455239e573c56d75dbacf05c5a41e992e99cd38f76/cle-9.2.213.tar.gz", hash = "sha256:696ec95931d17ec19686efc436eea88152a72d355fbcee428452ac8fadb94335", size = 226878, upload-time = "2026-04-28T18:04:57.661Z" } wheels = [ @@ -447,16 +516,16 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "archinfo", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "arpy", marker = "python_full_version >= '3.12'" }, - { name = "cart", marker = "python_full_version >= '3.12'" }, - { name = "minidump", marker = "python_full_version >= '3.12'" }, - { name = "pefile", marker = "python_full_version >= '3.12'" }, - { name = "pyelftools", marker = "python_full_version >= '3.12'" }, - { name = "pyvex", version = "9.2.221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pyxbe", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pyxdia", marker = "python_full_version >= '3.12'" }, - { name = "sortedcontainers", marker = "python_full_version >= '3.12'" }, + { name = "archinfo", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "arpy" }, + { name = "cart" }, + { name = "minidump" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "pyvex", version = "9.2.221", source = { registry = "https://pypi.org/simple" } }, + { name = "pyxbe", version = "1.0.4", source = { registry = "https://pypi.org/simple" } }, + { name = "pyxdia" }, + { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/c6/3017a258bdc1c9307173bd437d9c1547bec902fbd76e7db9e911e635fa72/cle-9.2.221.tar.gz", hash = "sha256:27d39e1cb852b8b19cf759effecebce01b7538b617d0b3453687548357692d8a", size = 423633, upload-time = "2026-06-03T23:38:47.848Z" } wheels = [ @@ -476,6 +545,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/77/ab9f823f42e829248a38ccb028188565e49cf4c5f532f45699f6df8bee15/cle-9.2.221-cp314-cp314-win_amd64.whl", hash = "sha256:488dac1934657456959bde942076415886cda040afe43ed1609f0d4759c37712", size = 484940, upload-time = "2026-06-03T23:38:14.79Z" }, ] +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -485,6 +563,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, +] + [[package]] name = "cxxheaderparser" version = "1.8.1" @@ -494,18 +629,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/3b/c20ed70839937311fa5a7cc511be4e369d7143b1e13366ba78e6bff53c45/cxxheaderparser-1.8.1-py3-none-any.whl", hash = "sha256:0f58c74b9a879b49a5df680a6c501f23c0d49c0379d7a89c14f62d0fe0b456ec", size = 62128, upload-time = "2026-06-13T19:44:20.04Z" }, ] +[[package]] +name = "dataset" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/10/0113e396ac92652cfab96663e2a98eb8ddf33766c24c586d79d3ea87eaa3/dataset-2.0.0.tar.gz", hash = "sha256:0e13d309dfbaafb3e67b991605ce6767867262908864455505c415442c0bba45", size = 183331, upload-time = "2026-04-12T09:59:37.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/22/2a12be390bb1bc92e6930a9180f27a4cba002cb09fad9b68b03ca345e94f/dataset-2.0.0-py3-none-any.whl", hash = "sha256:c08354c068002c2beb7811b37b8abf434e1fc333898f8cfc2be32df3f14099b3", size = 21126, upload-time = "2026-04-12T09:59:36.741Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "droidasc" +version = "0.1.1.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "androguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/a4/f352ffa2533dbb75fc9620112a8386cfc60abc9966ad3d7366e4fadfc599/droidasc-0.1.1.post1.tar.gz", hash = "sha256:ac38066e43f1df5a32a3268523df45cd83103de4426e239498d1033b7e0db509", size = 89334, upload-time = "2026-09-16T18:50:55.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/b8/a6c266f294ff16e26dae9594d11580ffc05a2f4ec91fdf8c219b201ee02a/droidasc-0.1.1.post1-py3-none-any.whl", hash = "sha256:5034b5bb70879580cf579af18f70e15b8ff2af3326fddc85c583d3c7b65c2092", size = 95814, upload-time = "2026-09-16T18:50:54.272Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "future" version = "1.0.0" @@ -539,6 +717,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] +[[package]] +name = "greenlet" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4f/4af258e1ce388eb64be82d6466b7d0b3bc3eede154e14c3360a76a759b71/greenlet-3.5.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:95e7c44d072db623a1aab04ce488cf9533294a77ed9d072cd503a3596f4106ac", size = 292967, upload-time = "2026-09-14T14:26:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/1d/05/dc0d54e90af2f192724936799cee990687792ff54a4638d3ae0e0a0145c0/greenlet-3.5.6-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7d501d5eb5d4f67207df364752ad697465b834268744be7581c18d81d35d41d", size = 609284, upload-time = "2026-09-14T15:11:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/c1/51/826b4fe7bc39c3f910c95e889a981c827f72a611fc035f31816a256cf043/greenlet-3.5.6-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a364c1ea75dc51b83a17f52fe0c79cf8bc4ddf740403bebd4581c7666eea017d", size = 622648, upload-time = "2026-09-14T15:20:39.198Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/092682ae7ca1aadd44aa36b0bc35c48c9ed55f9ffebb5794980b1e2ae74b/greenlet-3.5.6-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eed88b64a5e5da72d6a71cdc5aaeefaa5ced9b748f8d19f89800b339961dad39", size = 622813, upload-time = "2026-09-14T14:35:54.845Z" }, + { url = "https://files.pythonhosted.org/packages/07/59/9c6b723a559b53bee0980a5dc57577d434891dd454f0fe54ed6e22d4e4ce/greenlet-3.5.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:874cea8bb1ec1ddccbacbd027856f6bf496f6bc18aba97a918c20e067edab236", size = 1585817, upload-time = "2026-09-14T15:10:03.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/d4/2bceaa305ff0ecf99511480da8f0a866eb2469a68da8715008a0d3e8f65f/greenlet-3.5.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:128813fc29f2336a21b4d06eedd5e16bcc7ea46f59e9ff1cb30ea70e48195d88", size = 1650177, upload-time = "2026-09-14T14:35:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/7a/33/c57855a6abada0c7bfb9c6c0f33df1fbed8e5224708478dbcdff6c5db490/greenlet-3.5.6-cp310-cp310-win_amd64.whl", hash = "sha256:dad3d233d441a022c1f7155f0fb9d5aff7b97c1ea8c7dfa02cce586b16ab2d0b", size = 322896, upload-time = "2026-09-14T14:24:52.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d7/41511ee2696f14be4200b524d9553dc4295e2bdeb20aa8962c3cb25e71c6/greenlet-3.5.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a6a4b98a9132e0f45c9fc245a63894cfd8c45fb7a0d6bffc5eab3ec327cf7324", size = 294075, upload-time = "2026-09-14T14:25:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7b/b509624970909294cd064ff7346148ca9941c21bec9026d7873dd254e9fa/greenlet-3.5.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45bfd2b51e38aaa5f9849f114d9c7c1d75f69187c849b3549cd64c465283abfa", size = 613429, upload-time = "2026-09-14T15:12:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5c/d2eb503067f9ba20875ef8c87681f29a64f53bbbbe4059a5d7c53179d442/greenlet-3.5.6-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c6dede9133e1da41d561bc3fb14e92b47e2ce39ae60edefaad145658ea7c5e2", size = 625405, upload-time = "2026-09-14T15:20:41.053Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/63d4477ce31dff2fd802a9a20240f6606aac85977e0fb18443aae33de3f6/greenlet-3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c20ea32a73d17b9b60e3371240e17b0068120c98a5ec01a224a7dd8c89733ba", size = 624428, upload-time = "2026-09-14T14:35:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/ad/aa/9cde4e00688eaa2a03b91d12e4681439a87e6aad860399e0847af6a014ca/greenlet-3.5.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a0b2791239c99992a86c1b635b787fe2a877d9eaaa26f8891ce943832b585ae", size = 1588385, upload-time = "2026-09-14T15:10:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/24632b5ec186b64e21e07a8f53ce5e15a7e9cb33eddee99a5fe16379afa5/greenlet-3.5.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:188bf333769b7145e2b0b4a7f09615ec550ed44d3a2a8395fb7b36f0e9901e13", size = 1653119, upload-time = "2026-09-14T14:35:48.275Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6c/019d2ef898f4b9ac845167f1c6f73229e9a4e2439362a5e2ce50205a19b0/greenlet-3.5.6-cp311-cp311-win_amd64.whl", hash = "sha256:a6b4ff33f7e011bbaa148238d131c4fd4f8afbab3c104ddfbdb2b12b74ff7016", size = 323317, upload-time = "2026-09-14T14:22:38.836Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7a/439df999455e3bdf02b1c68f3848d4020385ef0a01f89f706b07bf148a65/greenlet-3.5.6-cp311-cp311-win_arm64.whl", hash = "sha256:59deccd347735a7774223b05a93773fddbb298aba3cea21be4337fb4752dbe32", size = 307739, upload-time = "2026-09-14T14:23:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/72/18/3fc6d951466ae9a2a688edcddde3b2e388da0a8244e0caf7117bbeb0eb95/greenlet-3.5.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a5876d0a60355af98d535c47f6cd6eb0f8a432396dab26845d380b92f8412422", size = 295668, upload-time = "2026-09-14T14:22:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/27/89/366d2af5061eeefa5012f510d95a99c8620dcc457609838db4d538820318/greenlet-3.5.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e85880b538e59a59f55117b81f208a6660ad5ac328aad9305f812d9b8bc67a0f", size = 611700, upload-time = "2026-09-14T15:12:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/07f133f865fd58ae593dd2bbec3144acaee9b04ffe2eb48c6e121747ceef/greenlet-3.5.6-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0ba7c2a329d650628f4c8572fd1db29f0a59dd70a3e3e0710dcf18a35cce9d8", size = 624223, upload-time = "2026-09-14T15:20:42.459Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1", size = 621404, upload-time = "2026-09-14T14:35:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f5/33e5c9e48178b9259fd000f8f45caa4a65036f65d3d0c06a602f570f025d/greenlet-3.5.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0616b8f878098c5681fd8f0dc92d887551717402342a70f0abcbfea5f5ad8a44", size = 1584998, upload-time = "2026-09-14T15:10:06.653Z" }, + { url = "https://files.pythonhosted.org/packages/ef/31/9b4e140bc24d0ad7927ebd651f5608b0acc2334d061748c3b6ad19085cfa/greenlet-3.5.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb4596a6a4e5d47121a33ff20533a81e60f302d9e67b69909a8bc21a43f0a7", size = 1647568, upload-time = "2026-09-14T14:35:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/c3/71/d79f1791f824f8ff15c2978746640467ae932a2365e0201069f7f272395f/greenlet-3.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ac4abb3877c43af320392c664774eef6fa2cc063c79a55fc02d844a3cbe7395", size = 324203, upload-time = "2026-09-14T14:22:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/42aca4d56e8cb321912203069d8d34734cb288222f10ad2ae102718cc577/greenlet-3.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:301102a49120b095e72a7838792b41233975fc1c155daec6d98f81c00c9280e0", size = 308310, upload-time = "2026-09-14T14:24:03.008Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" }, + { url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/66/c0/d254544ae2b8bdd311aef000fafc02828c2771b17d994b3075620ea7cc6e/greenlet-3.5.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cddea1b8339451c2fb3388e138347b6126744f33b611bdb55b7357361cfef46", size = 295221, upload-time = "2026-09-14T14:25:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/18/18/eb54be16b9cc3971e09ca5b73334e1b8c804a4630d9addaaf218a4fe300f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c59acfa8eb73a1e0d484392dc002bdf001fd4ce73394e0132df3d1ab6093d7cb", size = 660992, upload-time = "2026-09-14T15:12:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/e193efe65671dcf294bc51fcc59efb52d154adf8612c4ea016da0d2c486c/greenlet-3.5.6-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3b4a01c6da07ef9f80d4fe8933b994bc99747bcea3eab0330a9c34d3c12655b", size = 673428, upload-time = "2026-09-14T15:20:45.756Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/28fa7a9e50f2859466214c4ac584d776db52c1604ad4dd158960a5af2a1f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a09d59bef1db94f384b5bcc2d523694d338f3df6b757aeeaf7baca5d0c0be88", size = 670773, upload-time = "2026-09-14T14:36:02.577Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cd/fb7d6cdd86ff3427c1494854f0e35437eba05142be91f530f6da75e09e19/greenlet-3.5.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b7c73d1cef3d9ae963e9ff03f6222df43efbb9054ffd2f1969c935b7fc84c02", size = 1631900, upload-time = "2026-09-14T15:10:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/143bdbb20a516628cb15074ae52ed17d850b450292609c7a6fccac6dbece/greenlet-3.5.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8b27df301f56e3b3d2298095c8f7d6b68f2521f6b1693e901fa039bdbae34424", size = 1693740, upload-time = "2026-09-14T14:35:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/019642432e6ae283301df1361227d47610709d2dc69a38f95edef266d713/greenlet-3.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:f8f0bd690e1a41294ac87905e8121c81a3761ec2583c768f13467428606c8c7a", size = 327473, upload-time = "2026-09-14T14:28:12.948Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7f/8aafc7bf70c948786dba7221d0dc0838e5329bebc6d434ef2208b4f0e760/greenlet-3.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:8cda13494d86a4f12429641117cb6ac4bbbc9c30a33f711f7d3a2e5fbe4b0b7e", size = 311095, upload-time = "2026-09-14T14:28:00.7Z" }, + { url = "https://files.pythonhosted.org/packages/14/7e/7a205688a5b3074933b18a906608d46d106e9a79d776bdab5a4abf4b4feb/greenlet-3.5.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:97c5a53e8c1754df58e73f047a99e287d4da1bdfe64b0072fb25c87000897951", size = 305352, upload-time = "2026-09-14T14:21:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/cb/9c4a57a9d9dd0256e20b8f7f4f06554c2c92badebf0ab73ce344321b78b9/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea4427d1ffdb3b523d7daa6712038428a4c16c450b9777bdd1221cfee0eab49", size = 672671, upload-time = "2026-09-14T15:12:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/97/52/c6729681ebbd298f4decd28746815acc8a0b0a0fde21d2df33776fd4d042/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73a29b5ba642e35433166a03a3e02935e7238c4b3467fbd77523b99edea23e5b", size = 679489, upload-time = "2026-09-14T15:20:47.291Z" }, + { url = "https://files.pythonhosted.org/packages/58/c5/2b6c721ba8b8963da42d5a0f57f25b8aaeb1fe9bdd156875e57f3be648a2/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e70b033aba8ed47e2ac9b5d0d2157b05a34fbfa30a241400aef4118902cdc", size = 676608, upload-time = "2026-09-14T14:36:03.959Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/0d018e0d05bcdde19a0fcb907834155f1fc853a9bedd3f3f5e6acadcae19/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca80a49b53ed1d22f7282da7255f7bb2fd1935fd0f623d8613fda38745f18961", size = 1641479, upload-time = "2026-09-14T15:10:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f02ef9073919158f6403fe3701d4ed4403d646720e7201dfc6e9d264bac3/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:916f92f2a8db10508f739d0b5e00b83defe5d1115a997c54532a6d7cf8c95404", size = 1698758, upload-time = "2026-09-14T14:35:54.336Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/1f48fe647473a2dcccfd1839b2ff2c78eb57009be776b4da071e901c9bff/greenlet-3.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:886bcf1870af74c32bc310fd00a6b803445e17e51b7d5a107c7b35c0f362cc16", size = 331574, upload-time = "2026-09-14T14:27:18.451Z" }, + { url = "https://files.pythonhosted.org/packages/cd/72/3882855a75838faeb54a58aeef4fd77d20b2a86d4bad570c70d41b565dcf/greenlet-3.5.6-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:3ac3494c381dab876cad7d0b22f3a722f3e0c8deb3a65b9e7f35ad7f58b8fcb3", size = 295926, upload-time = "2026-09-14T14:27:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/10/1f/be4d957d8a9b90bcbe8db206548a42134d96222d43e5ed3fc4708fb6e24b/greenlet-3.5.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602024dae6d77e161f4b89491b62ca1d4f19949d79d47b2db057e476d21179d6", size = 666910, upload-time = "2026-09-14T15:12:07.901Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/60d62571a7d6de961e4ce7625d6c2faf359345659fc782d2cdf517c34577/greenlet-3.5.6-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8e63209c3e1e828ee6a457529b4a6d8b05d050fe0ae03a7ae49e967c5d312e0", size = 677415, upload-time = "2026-09-14T15:20:48.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/16/ac9e547b611539aaed1870eb1d6ddc57abdd5924b3a99bb9b5f0b44176b8/greenlet-3.5.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccadce0130fd813ec86ebfe969a6c58b42acc1d0fe55a47525375b740e07b605", size = 675927, upload-time = "2026-09-14T14:36:05.34Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/b7ba08d6431121741f1d30be0d5d292e76873325179a63586cd9217b62f6/greenlet-3.5.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9297fb9c39b9a2c039dbcd306c410bd6906b95244dec3bba4318d36c718c164c", size = 1637236, upload-time = "2026-09-14T15:10:12.442Z" }, + { url = "https://files.pythonhosted.org/packages/af/c5/3b1cbc68f0c082022fc8717f7fe4b8b13b8d583c52352be37f4e9f55bcd2/greenlet-3.5.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b374e79ffa7511afc11773aef40a4ccea6191fba1c856ea2f9c56738dca69d7a", size = 1698526, upload-time = "2026-09-14T14:35:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/12941ed2711400451c89d544e10f831800a2770f19dd55eac8f0f7f2003b/greenlet-3.5.6-cp315-cp315-win_amd64.whl", hash = "sha256:7969bffa322c097bd46ae595ada6a931cefda613f18ba64587e9cff4cb320756", size = 327739, upload-time = "2026-09-14T14:23:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3b/576b9ed5ac929252e340cf60b4bcb6a8515350dc20797064b1922dc4ea75/greenlet-3.5.6-cp315-cp315-win_arm64.whl", hash = "sha256:8dba0129b93e7091dfefaf4cf7000172741bff7f47bf6326fcf17f32fbb54d6b", size = 311715, upload-time = "2026-09-14T14:28:25.154Z" }, + { url = "https://files.pythonhosted.org/packages/16/c2/86cfc5555a98e12b86966ddbd24fd39af32f71f2f785c6595b7feb2db156/greenlet-3.5.6-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:de3de000d459402cda015068fd135aa50c0bf6f2477a80d4da1e646f123b4e78", size = 306244, upload-time = "2026-09-14T14:27:57.565Z" }, + { url = "https://files.pythonhosted.org/packages/14/6d/83ffc9d05a75a80ab3a7595dbb1d9604e5d4fc2996d73a8ae2dbd1284900/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45663c01a4de48b9a64a2ee1509d92d1dfd3afb02b2ccfc9333029d11aef996a", size = 676578, upload-time = "2026-09-14T15:12:09.468Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d6/c2cf684810e5caded075970aaadea654ecb58b8382b9aecf1d231b936894/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3deccbb57a481e3a408fe61cdfd5c13e0678fc0a30fdd09597917ca87b4be877", size = 684203, upload-time = "2026-09-14T15:20:50.261Z" }, + { url = "https://files.pythonhosted.org/packages/62/19/00e1bee5d2af890dc8f400b54d0b0f9b489965f92bc12b407ff72cc6f469/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311018b46472fb26ee85870847fb89eb64cc8aaddb617400789d87076f7cfeec", size = 681253, upload-time = "2026-09-14T14:36:06.742Z" }, + { url = "https://files.pythonhosted.org/packages/89/58/c9275fd0ca195d1d3402931bcce8cfcc74726ff76efb1883d229e6e1a3d7/greenlet-3.5.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7f924a5a9d5890649566f2f6682e0d8ad8ca23028bacffbbac36dbd7fd680176", size = 1645988, upload-time = "2026-09-14T15:10:13.758Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/b35747582fa4f1a5453f8f3002405dbac788e450cec7674dc2d204b6ccb5/greenlet-3.5.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:de9923832f2d8c1a5ecd8d7260465a6ca5a86888a0d129e3bd5cf0406d2fc5bf", size = 1702383, upload-time = "2026-09-14T14:35:58.143Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/6ec22ac9351e474d2a134d0ff9400dc80362d1c20f0721088ffffdfc205b/greenlet-3.5.6-cp315-cp315t-win_amd64.whl", hash = "sha256:2ab5f42ac6c238eb71770715e6e909ad9a1a92b6c681ccb64cd5a0f07edb953f", size = 331845, upload-time = "2026-09-14T14:27:41.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/697c051fd534e223461fb8b523890e21a24eeca229cd50624cff6f02fabd/greenlet-3.5.6-cp315-cp315t-win_arm64.whl", hash = "sha256:f9fe868463ec7e1363733af77e38a5fda3e9b63940337048c945d69e0c80ff24", size = 315070, upload-time = "2026-09-14T14:22:21.476Z" }, +] + [[package]] name = "ida-domain" version = "0.5.0" @@ -571,6 +819,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, +] + +[[package]] +name = "ipython" +version = "9.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/32/99451b1283ec5d92ad77073f12e1c667dc10775384d8f15c2914207149dd/ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529", size = 4539289, upload-time = "2026-09-01T08:29:32.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/1e/65b59cf518c106aa755e7f7da3099027738687a862ec785060702a481320/ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", size = 639038, upload-time = "2026-09-01T08:29:30.673Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + [[package]] name = "lmdb" version = "2.1.1" @@ -610,6 +933,185 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/c0/f617cf8a6062b3343151ed45b5e093daff8ec2107d72ab637ffc9bd23970/lmdb-2.1.1-pp310-pypy310_pp73-manylinux_2_38_x86_64.whl", hash = "sha256:8ff72667a8cef22a71fbd91b4c078b94ab45f38881b0bb64481b4477fc779d80", size = 91155, upload-time = "2026-03-19T13:56:24.204Z" }, ] +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/ad/28ecd7cb894d172f3c9c80a075eeeb2017ac62e3632cee05a5f9493547eb/lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21", size = 4211198, upload-time = "2026-09-02T14:48:02.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/6b/a7d5c08e19a8e69887ed722fffaefdbaffc8959d5ef5c370a65e52c895ac/lxml-6.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:40bcbd9f94166ffe925811e730607385cec959f42fb1bb7dad83748680465221", size = 8575497, upload-time = "2026-09-02T14:46:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/96/dd/c25a32f9f6039a96cfd52296a4630075868aa16e71858b3076699a059201/lxml-6.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:05f5bce9af14fd1506997594bd81cee6d9c6b58ea80a39c058327aa6371ed9e9", size = 4619233, upload-time = "2026-09-02T14:46:08.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/d49375a47644369d84f90a9fe4ff1924faad58d4f95563831eca84ca29ae/lxml-6.1.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff88a92cafde90888511242d1c54afcc1a8adbb6dc0a88fa7f87e29e92400d4a", size = 5015387, upload-time = "2026-09-02T14:46:10.797Z" }, + { url = "https://files.pythonhosted.org/packages/76/0f/d1b1f52925442f7b4b1abd81a41905987322f6df6a5dd42fab8579415828/lxml-6.1.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c00e26288784460885fe76e4d4b293573e0f791f52e6d60e27b42edf005922eb", size = 5168571, upload-time = "2026-09-02T14:46:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/e5d8291a68a27e564e4e1eefba08c3844c6800bcb43f3e72a32b20971132/lxml-6.1.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:773062aec2f2e56b2b22d37054123f0de8a22a4688a0c3376c3fe42685f975cf", size = 5068024, upload-time = "2026-09-02T14:46:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ce/dbea34cd115ae9b8ef53816daa912563615adf4daed42531878a2fb29c77/lxml-6.1.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6449672f9c93316deb5e2839e18931f468670e44d5bd9b1301a5a9655d45c07", size = 5296830, upload-time = "2026-09-02T14:46:17.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/12a2ab6e8c8afecb82a3f0e9a518952b6a1cddf405ad8542883bd71e6096/lxml-6.1.3-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:ec295280f4b37769256da025acf5890370355ac589c27e89caae0b5e9eedc702", size = 5424696, upload-time = "2026-09-02T14:46:19.706Z" }, + { url = "https://files.pythonhosted.org/packages/02/3f/5670e198266c764595687a234fdaed33837f487b95a596262b2548e48933/lxml-6.1.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:5929d9df5e7e3379183be0e21f7d559618a5b61cb63280df6164019242e337ed", size = 4783635, upload-time = "2026-09-02T14:46:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/70/24/007ce6b7bffb61a6ca88c3a8f21b26f3f0aa3b3f6bb648a56e328c994a14/lxml-6.1.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e1eb8a4cbffd5553680ad96be6680e364710656eced73d1dc90ec489df599a3", size = 5373212, upload-time = "2026-09-02T14:46:23.572Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/cf09f38cf9005bd5cfa4fd452b03b290b1c48c403fe0319a8013f4b3cae0/lxml-6.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16148acd77ed1d8836a56db883af2f5eed720f9723088110b16a0d08582130a6", size = 5116476, upload-time = "2026-09-02T14:46:26.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/83/eb021e5db4336f0bb1438cba6f053ea135aa00b9f4ef0439473d6b986308/lxml-6.1.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:23c366231259cd75ad06495174701afb3fcb36a92917fa47de2d1f1bd9d95739", size = 4814172, upload-time = "2026-09-02T14:46:28.3Z" }, + { url = "https://files.pythonhosted.org/packages/c8/4e/147b6f9088cc191713249ac547b0af2fece489c8cdff1f2801ab47dda8a9/lxml-6.1.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:da85db328e507da922d586c3c7416ec360ec22e9cd9e0700691afacde0c81f53", size = 5361711, upload-time = "2026-09-02T14:46:31.035Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d9/8cfdac0d7d771e25af2c1f4bc874032f025a4b59e0b6917c3c7858070795/lxml-6.1.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f17d83c48ee9dfd96abae3ac3e2108c76d2fc86ce96355e37b8da9f7f4ecc08", size = 5321598, upload-time = "2026-09-02T14:46:33.165Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5b/d2413c71f312dccdd07ed985be356657fc624d822ba7e2c87e8722646156/lxml-6.1.3-cp310-cp310-win32.whl", hash = "sha256:7dd624c1eaa629ad44b59a1a0145fdf2d67895592dce94c9358b938b3d075e65", size = 3604471, upload-time = "2026-09-02T14:46:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/7a/bf/74b6785beac6488fd395e78796339bc197fbad6fd6103b41b15a4009dc4b/lxml-6.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:18a4db52b5a7b53a3540b0b0f4123319334621ee8083d496de314d0bf06ff59a", size = 4029086, upload-time = "2026-09-02T14:46:37.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a5/ddf6e1744cd76fc9f0ce11cb16b117d6eaac46ebaeca01968e9014e8770c/lxml-6.1.3-cp310-cp310-win_arm64.whl", hash = "sha256:0feebef8d0521188d0157f758356072e840173aa61ca45b8b3f87959ac283dd5", size = 3674608, upload-time = "2026-09-02T14:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/f1/95133bde7af7afb1f5ba6090b674d826b7a518318bba54bbbb633b27865a/lxml-6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11", size = 8563141, upload-time = "2026-09-02T14:46:42.334Z" }, + { url = "https://files.pythonhosted.org/packages/80/54/5a79ee2181ac773ee13e48205411845feec69e1c3d097e985c1343171712/lxml-6.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4", size = 4613690, upload-time = "2026-09-02T14:46:45.253Z" }, + { url = "https://files.pythonhosted.org/packages/ab/29/8c24672f56807f119312f073f24204368574bd16b384ede861b5104b3a2b/lxml-6.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b", size = 4935630, upload-time = "2026-09-02T14:46:48.071Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/ce2436d854c848c19fc9287143991f3fc76b8b4e9a0dbba8452e51dff264/lxml-6.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13", size = 5079033, upload-time = "2026-09-02T14:46:50.483Z" }, + { url = "https://files.pythonhosted.org/packages/91/ec/b66f66f6499ad800265d57540b51e6632e3232d3526f42f2f8fd4b14e0ea/lxml-6.1.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41", size = 5012298, upload-time = "2026-09-02T14:46:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/94/2a/25d128872f4d51753542bfc3feb482c2ea7c8a2d6d81a0bc5c6a00779ed4/lxml-6.1.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0", size = 5211431, upload-time = "2026-09-02T14:46:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/0a41bbef074a556110f84fafb6d8c2998293c7d3bfbe1ce74515bc65393b/lxml-6.1.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867", size = 5343417, upload-time = "2026-09-02T14:46:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cd/16116c3f91791aeeeab1cbe6e7eb6e646f127be7b0158b262eb526a21a0c/lxml-6.1.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054", size = 4673219, upload-time = "2026-09-02T14:46:59.604Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bb/4dff849f443ef70221676aec938bc41e8bae6430aa2ca13b041319e14b98/lxml-6.1.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6", size = 5281246, upload-time = "2026-09-02T14:47:02.375Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ac/4aa7dd059420bfd35278c7fe819e9d319ee36a0453b7bbde1907a7832d91/lxml-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c", size = 5055451, upload-time = "2026-09-02T14:47:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/44/20d90cf6f4234de9cd9eeb4f519419885fdb087fa80d073c7b57be342021/lxml-6.1.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48", size = 4722694, upload-time = "2026-09-02T14:47:08.461Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0e/6bee12325e53dd6613fe1e107def07583b6182ade03e94bfef8976622e44/lxml-6.1.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0", size = 5269179, upload-time = "2026-09-02T14:47:10.647Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/54d269ce5cd0787c0424d9cef449ee794d4097725d13dd2acd6181c44e9c/lxml-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12", size = 5235559, upload-time = "2026-09-02T14:47:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f7/5a3095f187f1bec293591616a1677781acc265c5b313c009f8a19c471a09/lxml-6.1.3-cp311-cp311-win32.whl", hash = "sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633", size = 3600377, upload-time = "2026-09-02T14:47:15.957Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/15531a0d307c96282fe8b639b3d74e8bd783e4ab4cb2b0781146ac4161b8/lxml-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559", size = 4032700, upload-time = "2026-09-02T14:47:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/8de76314955545ceaaa7c0305017b8aaa217905dee59c62c0e2c1e44a68f/lxml-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6", size = 3674431, upload-time = "2026-09-02T14:47:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1f/a180b57d9eeabaab77f9d5aa30356898ea749c4795596a8f66d1eb6bef2e/lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc", size = 8602094, upload-time = "2026-09-02T14:47:26.054Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/070c92013a1c029a602b03560d68772313d918268667fa993da7961759c9/lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d", size = 4638308, upload-time = "2026-09-02T14:47:29.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5", size = 4939696, upload-time = "2026-09-02T14:47:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11", size = 5105247, upload-time = "2026-09-02T14:47:36.734Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a0/a1f7f1313795bfec67b77f01ef3b1128d49f2d7f66a8413fa55d47f4e25f/lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a", size = 5011915, upload-time = "2026-09-02T14:47:39.846Z" }, + { url = "https://files.pythonhosted.org/packages/b9/78/840e7e3f1d0cc7a5cfac5d8505b97e25b6427fd774ac4bae672aaebfb4b5/lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32", size = 5638175, upload-time = "2026-09-02T14:47:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/0a/20/e022dbc6b4753a9bc9fc5fb28a27163430c1731b9913997f6544c1b2518c/lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c", size = 5244675, upload-time = "2026-09-02T14:47:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/83/82cde81d2b5eb38d1539fdfdf318abdd014a7e604f4df01c9cd3deb18f2a/lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56", size = 5358205, upload-time = "2026-09-02T14:47:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a1/f3b057371c8cb29f2a9c9c44ea320592446e40b74a4b0af68c3d8e65bc73/lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f", size = 4704495, upload-time = "2026-09-02T14:47:53.251Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/230eb28be5d412152ffc3c679b51fe1aeede5a53f3a8eb6e9748f2f4754f/lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5", size = 5255117, upload-time = "2026-09-02T14:47:55.963Z" }, + { url = "https://files.pythonhosted.org/packages/a3/18/1969f56763af24ce42ea156007b0b2d73fddea552e283b2010416394f0f4/lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385", size = 5054424, upload-time = "2026-09-02T14:47:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/2a90acc1f6fabaa3a8db9340437822bd8d041b205d626a4b3e8621aaa390/lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d", size = 4785572, upload-time = "2026-09-02T14:48:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1e/b90e845b1dcd0f2f3f26b98283d857f25909223aacd265eee032c34ab8b1/lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9", size = 5656516, upload-time = "2026-09-02T14:48:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/0a1b802c57f3fba5c4efd77d5c6b78adaa8f7b681f0c90456b140fe8bf6c/lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e", size = 5245982, upload-time = "2026-09-02T14:48:06.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/2c016fbceb3778137459292538d9dfa7e3ad9070fe409c15254ddd90d2cc/lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5", size = 5267340, upload-time = "2026-09-02T14:48:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b1/736d18fd6f0835761923b7bac1f0c27d60c1200384e9093f05d8c5100525/lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c", size = 3602606, upload-time = "2026-09-02T14:48:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/6ed903e4e6278a020c8a6f0dbbe78030d041840a6b4a64ea441a1e414077/lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c", size = 4005999, upload-time = "2026-09-02T14:48:12.51Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1b/7bcebb7b6332cb3ae85e9c13b139adb6f23f75c71d84041c56a5005d9a29/lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa", size = 3666631, upload-time = "2026-09-02T14:48:14.567Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/3ef45db776baea068044c799bbba68f3ca00a440c0e930a17c572f3d9639/lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd", size = 8590357, upload-time = "2026-09-02T14:48:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a5/eee2fc77eee5ea68e4a4334b1def1781a3beaeefd3d98e81b4a38dc447b7/lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1", size = 4632616, upload-time = "2026-09-02T14:48:20.745Z" }, + { url = "https://files.pythonhosted.org/packages/35/42/df27b56848acd29d8a720acc28977911aab36f2a09df4208d5502e887415/lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d", size = 4936186, upload-time = "2026-09-02T14:48:22.94Z" }, + { url = "https://files.pythonhosted.org/packages/ab/8d/8a7b91df0b54d09d25f5f44885d6b3e0a6d6643a8c070191580318d20c42/lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed", size = 5093324, upload-time = "2026-09-02T14:48:25.132Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/8f340ddcd43790332fb0de8a26628d571a492da3300cd191821698407c96/lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2", size = 4998850, upload-time = "2026-09-02T14:48:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/9c5bb572f1f09ec9e4322bd4a4e9f4ad48347fc56ef94cf4df58a5279dc8/lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8", size = 5626813, upload-time = "2026-09-02T14:48:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8bf1fd8bae8247743968bb76d027a1ac5bd2c4b44495fba6a71b30d10706/lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e", size = 5232385, upload-time = "2026-09-02T14:48:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/6cef69ed81cb7df0d03b0dd09d08e6e2cf5061a743ff6f42f0b741548e9b/lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245", size = 5347088, upload-time = "2026-09-02T14:48:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e1/8e5fd8ddc8c7d685badb0f2db149e3c9da84eefc2827c01c658df2c4e3cb/lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0", size = 4707227, upload-time = "2026-09-02T14:48:36.62Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7e/00041382a11be40a88bf405ebff11c8efabd3de79f2691e1638b1c47a8a0/lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e", size = 5240208, upload-time = "2026-09-02T14:48:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/316538b5cff0936fa63d45d421c655730fcbb5a28dcac728c175083002bc/lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2", size = 5050271, upload-time = "2026-09-02T14:48:41.213Z" }, + { url = "https://files.pythonhosted.org/packages/c9/91/455bcccb3ac725373007344d351151810cd19762d1673b64b811f4359a42/lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310", size = 4780433, upload-time = "2026-09-02T14:48:43.779Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/580440e2f52cf00bba5c5e1080bfa88cdfcde73be71a11d95170ddbb663f/lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748", size = 5645928, upload-time = "2026-09-02T14:48:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/d123c1f244306543d545f62443f794959e4f1ea709fe100f8740d514e74a/lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d", size = 5231184, upload-time = "2026-09-02T14:48:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/fe55b2bd5c6113c906511cd88f6a470195c5fbff1124f19970ab706c3477/lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc", size = 5255814, upload-time = "2026-09-02T14:48:50.948Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a7/485df55acf55dc35e4ca89d2f48f03889e5a3241826b18b85102b32ce9d8/lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87", size = 3602214, upload-time = "2026-09-02T14:48:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/e46a7702bd95e9043291f7c3539b6184cba66f96cea9936f20939b284eeb/lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477", size = 4004091, upload-time = "2026-09-02T14:48:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/154c78e20479a43916e63f19cb720d83f44f024b03228be44c92d9a97b24/lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1", size = 3665468, upload-time = "2026-09-02T14:48:57.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/15/fc75a70b0af6021d0ea16811f1fc71cc42cd06ce90fe10f007a69b2eed84/lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165", size = 8609725, upload-time = "2026-09-02T14:49:00.156Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/398fcf9018f881ec9aeaafae1ddd6586dfb13314a35d35e899de373dcae0/lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d", size = 4639629, upload-time = "2026-09-02T14:49:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2d/49b6a6ad7ce8f64b07b9fe852ff0c6d3fcbb26db61bee4f63d4120180a1c/lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e", size = 4965074, upload-time = "2026-09-02T14:49:05.133Z" }, + { url = "https://files.pythonhosted.org/packages/66/bc/6230cf80e4331c33383b0b6b73dc31a393dd76edd4cb73d761de5123034d/lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8", size = 5099355, upload-time = "2026-09-02T14:49:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cf/d1143d9b7717e07a82f158a1fc9ce6e581fdad1226734950af869e3ffde4/lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75", size = 5036795, upload-time = "2026-09-02T14:49:09.65Z" }, + { url = "https://files.pythonhosted.org/packages/31/6f/194bb00ffb89712c30f5a7e1b8e685590e140fad6c8261fec172c09a3dc0/lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9", size = 5658740, upload-time = "2026-09-02T14:49:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/27e3cee3dcdb3b7bc09727b642bdbfcd098490ea77df04611db9060d7722/lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0", size = 5245991, upload-time = "2026-09-02T14:49:14.154Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/8312560579fc980bbd2233a8a673cc46f7d613d3633f2bf08a21e8f4ad13/lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6", size = 5354136, upload-time = "2026-09-02T14:49:16.459Z" }, + { url = "https://files.pythonhosted.org/packages/74/d8/eda60f4f73a9c780b5d6e1175484f66e6c81a2c93346e2906a1fec9c7a02/lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023", size = 4704379, upload-time = "2026-09-02T14:49:19.032Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c8/c9cc60057be78ac34bd2b842e45e6e88edbfe5e532e82c3b82381b7aab49/lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e", size = 5258676, upload-time = "2026-09-02T14:49:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/66894008fee8d1785b8db129747ae963fd427b68f456918df7f2f24a8b98/lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92", size = 5090069, upload-time = "2026-09-02T14:49:23.562Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/c1b60404859f4c3cd1f41f29c65a24e25cea78fde822d9574a21f66810be/lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48", size = 4741958, upload-time = "2026-09-02T14:49:26.037Z" }, + { url = "https://files.pythonhosted.org/packages/23/b8/6285f0cf546f14da2554cabdeaf7c2c2ff3190c74807f0de2e8810a786f9/lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d", size = 5683245, upload-time = "2026-09-02T14:49:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/2168cab44336dcb15fed0f0b78577225b83297cdf0dee349c95420c3dcb0/lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559", size = 5246087, upload-time = "2026-09-02T14:49:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/f5/89/32f5de69a0a31f30e6164981851f87b37ecb2c4ee838e504b88d49d4818e/lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415", size = 5269352, upload-time = "2026-09-02T14:49:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/741d952ed3a7ef7a50055c6415aec3f067015e97f72f4389ce77b09657ba/lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d", size = 3662783, upload-time = "2026-09-02T14:50:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bc/5811cc73cac05e324e05ba9b0924e1a163a317a167ede8a9c748b11db30a/lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861", size = 4073951, upload-time = "2026-09-02T14:50:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/92/18/3768c8b01ac3a9bed1914715e6011711b00e2a11628ffa6f7fa37f8e0269/lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376", size = 3749279, upload-time = "2026-09-02T14:50:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/72/38/84684784738d9451db2b330de2483f496690c3a5c642071df24135739b37/lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f", size = 8860296, upload-time = "2026-09-02T14:49:36.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/b7/fc4c50bb1b38e864010ea396046cabe85129bf9e65b11edcfbc37d356241/lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55", size = 4755190, upload-time = "2026-09-02T14:49:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/e2/ee9aa6ed2b666b2db1f6f7fd48964ff9da39ebe827ef5eac0ab881f639d9/lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2", size = 4979517, upload-time = "2026-09-02T14:49:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/29/e3/e7763d1661b283ddd4fa36f91b9a497db6b8d2aff55028b16c7f642e0755/lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626", size = 5115270, upload-time = "2026-09-02T14:49:44.493Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cd/22205d5b4d177e3f4156f780412426ee7c7f8107809f119f0dcc40fa51e3/lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414", size = 5032449, upload-time = "2026-09-02T14:49:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/06a4626c3bb79ef8c501b674afab8100d64e798665bb2a97d1c960636a49/lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17", size = 5603325, upload-time = "2026-09-02T14:49:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/733682a0c2de9f5779ba207bbb3f3f6be8c6bda863fc01739b186b38783a/lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473", size = 5229023, upload-time = "2026-09-02T14:49:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8a/e69cdaca3fd33a647942925664f01b20908d41a6968c182305be9c38fb11/lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37", size = 5317811, upload-time = "2026-09-02T14:49:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/0c397588174403c2ab68fc464abf97e03e7324f9c6cb6a99023104707195/lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70", size = 4646516, upload-time = "2026-09-02T14:49:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/56/7e/cfea25afafbe49db8b225764f7f74bb37c2a7f5e717d917d3d4a5e098ed4/lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7", size = 5240626, upload-time = "2026-09-02T14:50:00.279Z" }, + { url = "https://files.pythonhosted.org/packages/a1/75/7a587771bb52ebb0e2c57b6dbe9fd96a70fbb54d72ddd97d54c5f8ec18d5/lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2", size = 5086619, upload-time = "2026-09-02T14:50:03.245Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/94c0ebe6d831861542d251e038052e52bf6d33f1d18f1cfffdc82851065a/lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c", size = 4758828, upload-time = "2026-09-02T14:50:05.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/938d67bd0e5b1fdfa52be28aefdffbad57e1f6b8e921c2aab88542c75f40/lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8", size = 5627083, upload-time = "2026-09-02T14:50:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/d8/65/4e51522f6c214650db0abb7b16ccd11b1238b8a05a8d59aa4ebed59c9f67/lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb", size = 5235170, upload-time = "2026-09-02T14:50:11.255Z" }, + { url = "https://files.pythonhosted.org/packages/92/c2/e73d19365665f6b16ef84df21199befc3b06e4c539046ad2d9595f6fb9ea/lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8", size = 5252273, upload-time = "2026-09-02T14:50:13.782Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/7f386c84c9fe2854e1ca6e231c285e1c8f392971ac353c6865e6ec49faff/lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a", size = 3902712, upload-time = "2026-09-02T14:50:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/82/a6/8a3eb793f7900ef01c7f99e6f5fcbcfbdff35251cfaef66b32a4c16352d6/lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2", size = 4400979, upload-time = "2026-09-02T14:50:18.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/3807bea283b4fe9e9d9f5dde46a73df91178472b335d2778e10b2a37aa22/lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026", size = 3823401, upload-time = "2026-09-02T14:50:21.119Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/4614fcd65496054cfb7172662f3576a59200278739506433b8c241ea422a/lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0", size = 8609378, upload-time = "2026-09-02T14:50:31.772Z" }, + { url = "https://files.pythonhosted.org/packages/f2/51/2cdce3c65fa99a6195dd8fbd512d33407c1000ad99f63e0a285b63d7a8eb/lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9", size = 4640022, upload-time = "2026-09-02T14:50:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/0b30084e9eb1c546a4be3d9c56df70058d116b1a320400a59b0f7da87bf0/lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79", size = 5037928, upload-time = "2026-09-02T14:50:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/5c37275a3e361f6138dc06db748ea565c1fe8a5f4ee5e2ddd80047c81a89/lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015", size = 5661932, upload-time = "2026-09-02T14:50:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/70/c5/b71ffb289b15e2642e2a3cf6d468c44da39ea119061a99e5b05e3d10f217/lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a", size = 5249209, upload-time = "2026-09-02T14:50:42.141Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/9910da149a23932f9301652e57661cd9e42b0df18f12be21159b7255f92b/lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed", size = 4704543, upload-time = "2026-09-02T14:50:44.634Z" }, + { url = "https://files.pythonhosted.org/packages/76/07/9290329cd188c62e22021f79df04ee0cc33d9a93b0d38bd65ccd452ad9d0/lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156", size = 5261298, upload-time = "2026-09-02T14:50:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0c/aba78bd3401cd99b73a0aed8e2b9b43e14be94fab3603d4bbc8a62365f2a/lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d", size = 5090453, upload-time = "2026-09-02T14:50:49.952Z" }, + { url = "https://files.pythonhosted.org/packages/8d/dc/fa4426c3355aa0216cbeb3911495b5f65a26e0df85859a89928fe28f0396/lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0", size = 4744709, upload-time = "2026-09-02T14:50:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/be/2b/224fe7918658ab7c532ac2412f3c1eb28f71e6364fb07566262d0cc6a7b6/lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69", size = 5685802, upload-time = "2026-09-02T14:50:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/21/44/7d480819b9adcae5f84dd8ac529132c6b7a578544398225cd20321adcd91/lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0", size = 5249019, upload-time = "2026-09-02T14:50:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/385a267ea1b6b283f2249dd827ef360a295e9db14e13ef4665a120c60d64/lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4", size = 5271886, upload-time = "2026-09-02T14:51:01.667Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/f967b0eb172ae876855a402d6d9b11fa86e3e0c89ca9bbfeadf7ffbfa719/lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4", size = 3662894, upload-time = "2026-09-02T14:51:45.173Z" }, + { url = "https://files.pythonhosted.org/packages/f4/48/d8a8c4160a29e663109ad520bac2deb37fcd014756d024561e8bc3e611ec/lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad", size = 4074626, upload-time = "2026-09-02T14:51:47.77Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/3e1395d34d19f9254625d0b567b81cf70d37d3417be074f4d63b94a2be3c/lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758", size = 3749495, upload-time = "2026-09-02T14:51:50.663Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c6/7465ffd9c43883526a382df6fa4846c9d8d419214f7effbf65270e795471/lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe", size = 8857677, upload-time = "2026-09-02T14:51:05.109Z" }, + { url = "https://files.pythonhosted.org/packages/ed/eb/1f3a917e299df43c8162c3e6f64fc2cea3bcf277910f35bff5b8e5d39901/lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741", size = 4754522, upload-time = "2026-09-02T14:51:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f9/f81b4bdb6efb7a596be29603d8758154d00a5f545db9f3cef9d9041c8f64/lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300", size = 5033744, upload-time = "2026-09-02T14:51:10.633Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/26d9bfaacb319c86e0eca8a1a0bf1130d36a7afbd318883e23caea63763d/lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0", size = 5615269, upload-time = "2026-09-02T14:51:13.357Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/73675f3f4141350ed65d6fec533b107d4e802c5caa340cf111771edd86e0/lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd", size = 5236280, upload-time = "2026-09-02T14:51:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/ed260767e7977de463a0f91f3f4fffcab85c0a2a024a21ffe1fa442c2c79/lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e", size = 4650718, upload-time = "2026-09-02T14:51:19.102Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/e9839d03b1e767f2725cf7d7d81b80d5f3f9fdc10ad8827e2479311b046e/lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2", size = 5243376, upload-time = "2026-09-02T14:51:21.606Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/4606e347e2788c301f677004aa83e28d24da9fe663a24380122af57be6fc/lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a", size = 5092340, upload-time = "2026-09-02T14:51:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/3314a8661cdf30f493c55a87db283961dfaae08451976a2ca418958e1804/lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011", size = 4758768, upload-time = "2026-09-02T14:51:26.813Z" }, + { url = "https://files.pythonhosted.org/packages/30/58/3bdc577f78ea8b7d72d39a84506f7001d5b28728f43e5b84891e3b7d9a4a/lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5", size = 5649546, upload-time = "2026-09-02T14:51:29.453Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e4/652633de1a2395949ebb7a8fc7d089aba12a2b45f0fefbc9d29e3e3ab3cf/lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a", size = 5234874, upload-time = "2026-09-02T14:51:32.262Z" }, + { url = "https://files.pythonhosted.org/packages/65/a6/c4581d171de30449304b4859bbd3607e9b40da13c0f88b68e6097c8d785e/lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887", size = 5260043, upload-time = "2026-09-02T14:51:34.841Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/ed6ee6186a89e69ca4ea9658b2a278f46a5efe8b5d4db56c7197f18653fe/lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e", size = 3901093, upload-time = "2026-09-02T14:51:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/67/9d/11d10257a4a048d04195d638bb61f0246ce2448eb05f682bcbab25a257a8/lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6", size = 4395446, upload-time = "2026-09-02T14:51:39.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/44edd7de434181c582892e68d1ffe6775ca403ce14aea07cb5a218a936cf/lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf", size = 3822836, upload-time = "2026-09-02T14:51:42.471Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/dc1fdf3a53f84ca88b6e942277ddb47954844a0ececea8cc5fa3c1324831/lxml-6.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b061064b4a2fe8598a466d723d43dbcd5a610a5d5cfe02fb6226f5c17349f75", size = 3947704, upload-time = "2026-09-02T14:46:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ed/e36d547d6c958b5693b873504735cb4d0388d545945d66a7aed8983a720b/lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8499d464de86fab0f102313cce32a9bed9ab1f06ec813cf025cb790964fbb765", size = 4220149, upload-time = "2026-09-02T14:46:24.907Z" }, + { url = "https://files.pythonhosted.org/packages/98/54/7f51e6b6cc0755f9b5fc6637748279e9f48289d917b3a47ac9fedf3318d3/lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e67324961ac9bbe616cce5100514d2e34d88665aeb07071e8b16eac55d06d94", size = 4329391, upload-time = "2026-09-02T14:46:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9e/840b0d2e25c10c491b010d555b46e6e5264d3ad73a91557405fceb738c35/lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d12669a2c419b0e8dc423d23dea24bb82f6f9cb829f32e04674b0ba40322a7c", size = 4262125, upload-time = "2026-09-02T14:46:29.199Z" }, + { url = "https://files.pythonhosted.org/packages/69/8f/42a41571dfc772c12628747f883d24c978053856825b99d7a187117b8079/lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97acecb11cbc411473f15b8d780df06d7a9f3a2aad9aca78364f56640c8fb70e", size = 4410104, upload-time = "2026-09-02T14:46:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/f3/aa/27d93812be916f1f674b2035edd86d41c77745ff2ad84f58c25a7445a397/lxml-6.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f8b9c8ceebae6387d0dc77f7f4dbbfbfc962dba2efbfe6877486075a480726b4", size = 3510724, upload-time = "2026-09-02T14:46:34.122Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c1/2433176de263cc3f51fd2c303f993d5bb7f1da3139a0f7d168116c0bfa7a/lxml-6.1.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962", size = 3942969, upload-time = "2026-09-02T14:46:36.55Z" }, + { url = "https://files.pythonhosted.org/packages/7c/71/de7759096f480180fd9e43ff7c017860e2d2a9a43741ab093cbdf1820f07/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a", size = 4213008, upload-time = "2026-09-02T14:46:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/c2d09af47a34fa6c0c27473083812b449a411680bd04bbe609cde291ddc8/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167", size = 4322012, upload-time = "2026-09-02T14:46:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/68/f3/bf56fee0403ebd995be8e78ec9aca566016487d1b3cbf755ebea8ccffbdb/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a", size = 4257402, upload-time = "2026-09-02T14:46:43.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1d/6da9cc086a20d9dd6bcbf7c5d9575f0331cca9a05e67dab02d15e828170b/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f", size = 4410889, upload-time = "2026-09-02T14:46:46.975Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/91fe48856f9f8089be3096fa4dbe4b3fb5526f3bf3e852ea9497f399cb9f/lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae", size = 3511258, upload-time = "2026-09-02T14:46:49.046Z" }, +] + +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -622,6 +1124,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[[package]] +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 = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -711,6 +1310,57 @@ version = "0.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/6f/f036d4fb9f5a511262345bb18215c70fa69a250aca2e5ffe1a9c7e4cb85c/mulpyplexer-0.09.tar.gz", hash = "sha256:144e9e9bf66d3988f60542c9d3d4c94857438f7908f60e53f4c1cb1622fbbd30", size = 2846, upload-time = "2021-01-11T21:10:21.68Z" } +[[package]] +name = "mutf8" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/74/bfce3ab421b39503c6cd11a6bc812a763154082a4f69d55831d762e58fcd/mutf8-1.1.0.tar.gz", hash = "sha256:279796be8926ea53ac226a831727725387f3acadcfc5aa723f8a5ada559dae3a", size = 11827, upload-time = "2026-07-10T18:23:34.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/03/22e4f2f7feb962825dafa11a07d743ac67052c5aca5e1c80119f245a0d03/mutf8-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6767a8e1e3dea2b8d9c98d14906049b6b63dcddc87c79e60d9c38a72b19640b", size = 13189, upload-time = "2026-07-11T15:30:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/f3/02/8a0072308d9a0b60eb18c24356cc40320b046c376ae464cc47d8a610a86e/mutf8-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afd923aaf773542d2cbfb8dbe6cbc2045498efa1e72e86d1ac4d3aacf7e14941", size = 13501, upload-time = "2026-07-10T18:22:47.189Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cc/ad0e4c15567e58c9409e77f35bf209db0b2acfd715055c7a63e2755ec87c/mutf8-1.1.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:072ad993d20b244755a8d6ab0ba6fee5c974741dd0acf96df6cc585f0c3c61ad", size = 23408, upload-time = "2026-07-10T18:22:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/51/7d/31df8202123f6d61ec001b4612bf180a04d994988d5fb7c105ef229dd47f/mutf8-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea1eaa3fc7e9da366b9199d19540d311eb2c953436936b96c6c1f9e81ebf501", size = 24441, upload-time = "2026-07-10T18:22:49.301Z" }, + { url = "https://files.pythonhosted.org/packages/5e/06/8eeb7d5b713d95718cd556137161806a6da3840e972ec7018ceb85a2abcc/mutf8-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4aa9af7ce710850dae09ff5ef9bfcc27a3f48cd301b0b14d6b30a2ac12970cbf", size = 23889, upload-time = "2026-07-10T18:22:50.227Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bc/a9070513ffbc221a2983bf5657a55f47b26ef350f1218ff65b86bf1912ed/mutf8-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:350cf53ff2922e322be8141a3b67e4455dc7df2832545113039a85697bb1b802", size = 23321, upload-time = "2026-07-10T18:22:51.123Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/b9e45da2f00b0120f2f43fd1781dd139175bb5e1051e13af208858e639e0/mutf8-1.1.0-cp310-cp310-win32.whl", hash = "sha256:f7e59cbf2e959029aecbcdb8bcb64fb6db56bf62945c44cf9a8c7b495ad36236", size = 14471, upload-time = "2026-07-10T18:22:52.015Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/da17d272c5c393580a4038332f2a569be8fcc3dc168e5245de6e83213212/mutf8-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e3cb461153512a22567ee03a2e3104c6359443161ae83ec386107fb4d09124a", size = 14898, upload-time = "2026-07-10T18:22:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5b/4deca08eea67d85e6ba14e7cbe0060f16a04536119499e249ff5528efa08/mutf8-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:258e6fc37cbdb90f9ee571eb993b4632a6dd9d408159c33b72161cb5fecd00d6", size = 13506, upload-time = "2026-07-10T18:22:53.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/03/eb72e6d0587fc0daeb6162258efadf2ab3a3e90d2f07a7f88c7ca7a57180/mutf8-1.1.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8af3095f7afd5dfc32fb0a662eba659472357e83259b7b4f1f3298fb6d4f904d", size = 24685, upload-time = "2026-07-10T18:22:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/3a9456bf13ba225e588adf4b091c4053b3f20099623d7dcba9883e5252c4/mutf8-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbcc40de7b02f81a0c1a8bbd43ebbf9e45c7f8bf57cf9d46176c6cc9fa65e17b", size = 25606, upload-time = "2026-07-10T18:22:55.826Z" }, + { url = "https://files.pythonhosted.org/packages/3f/47/c27bc134ad4643303d99388d6b4762b42b734fd097d5d0614c390af47110/mutf8-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f748e3f7dcb9f6b7e642bdf6168b72a7fd5674182a827236bf8c7165672d322b", size = 25013, upload-time = "2026-07-10T18:22:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/05/29/31ff1ddcb34be6b6a80e84e0836daf2cce5de4420f6bda5b2dd555682350/mutf8-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d2a46bf14e16011a113b7b839e3be6c1d5ccd47bc5a4897799af83309cc4ffc", size = 24603, upload-time = "2026-07-10T18:22:57.766Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/dd5425ebc2c330d3b5dd69bb19d7d1ec7ab0984de30614c19066e0bfbe66/mutf8-1.1.0-cp311-cp311-win32.whl", hash = "sha256:ce85b33e06592c457163c03e1856e21504286b7d3cab7dbe45cc51b8931e3b01", size = 14497, upload-time = "2026-07-10T18:22:58.858Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/ab528d9f43c640eece2d3940368fa96021c9d227466752f377c21de0fe9b/mutf8-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf764c02c9c6a1806cb6523052340dbf1c45b676a180230e85ac85144aea048d", size = 14899, upload-time = "2026-07-10T18:22:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/83/35/a3074929620a649c6ad0557d55eeb25d2c22d1b2470b9d1bd9dd332bede7/mutf8-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d6682df2b8a7d6e8a769ae6b3295adce8ee032c020ce98f7bb31692b664882c", size = 13508, upload-time = "2026-07-10T18:23:00.662Z" }, + { url = "https://files.pythonhosted.org/packages/16/c2/667233e5871f4fd1cb4b4cfe6d540b5f88ff644b143bc78367cf68621eed/mutf8-1.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5e852720303c709f0b8150603032518818c56d378d17c403cb42cfe399b9bb8", size = 24900, upload-time = "2026-07-10T18:23:01.635Z" }, + { url = "https://files.pythonhosted.org/packages/18/0d/b234f2ba0a9b91cfaf9ce1386c967a0b8284e5b1e101ee8162cf09803dd9/mutf8-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e1abf471047c80793a0d29b4dccdc2b93e052245f38280c852cc14f966b1b28", size = 25811, upload-time = "2026-07-10T18:23:02.695Z" }, + { url = "https://files.pythonhosted.org/packages/26/63/5843c0b540884cee9b72658451dc156bcd1f30ea87ad4477f6ac48b1aa6e/mutf8-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3eb8e73167356159bfecbfbe30fccb5aa72167e4e56220e3e0ac0f34703a7554", size = 25191, upload-time = "2026-07-10T18:23:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/f4e3e6284ad5e7164c9110786f4498d5f4ac870e142d271f2d99d53c4281/mutf8-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:640f14ccb0b61c8f74de6d20291bac6512e6f1852888e3b9b2277e38291131da", size = 24809, upload-time = "2026-07-10T18:23:04.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/d7/13c3730942c8c93c8b856330e8976d6271f6dc05f6f40ffc59314316b6c9/mutf8-1.1.0-cp312-cp312-win32.whl", hash = "sha256:2cd6b7f78a1a9a1041e9c1b2b565cf6586aaa64cb0ab6fb20b3e08dddb759384", size = 14504, upload-time = "2026-07-10T18:23:05.696Z" }, + { url = "https://files.pythonhosted.org/packages/de/91/22f6f24ed635082c349a53893cffb149b61d2223be6a7651b23d6747d1fc/mutf8-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:fce9b80cd05214a2c886ee936e587db381ea10f4c4d45ba81689c0b9b76e6ebb", size = 14895, upload-time = "2026-07-10T18:23:06.562Z" }, + { url = "https://files.pythonhosted.org/packages/16/df/dd8db9d0318e1dbaf98cac4acc6b4a83efe4cff130b561f643bdb05a6e65/mutf8-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fe4c83cacf6fbd230a5a429a51afcdf56efbb7692938358b9eb572ef0cfa1f5", size = 13508, upload-time = "2026-07-10T18:23:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b2/042128bbcd8df8388fa370686a2ee1f770aed44d1f9c8d6aeda63cd10a8d/mutf8-1.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7569229befe0317125802d9220e5735771e3f0ac90bda0fa4ef2dac7d27bcd3a", size = 24895, upload-time = "2026-07-10T18:23:08.572Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5e/f3d343ac37c9818f1e7a74675b040439b024b68458a9c07265a32007d541/mutf8-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f904baade198aaf87910166ac0e0d8c42970a63ccc3d230e7fc626456cf22d30", size = 25809, upload-time = "2026-07-10T18:23:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/946842eaea8772f99d44f921eb25322d3fca6126a799607a26c4a3fc19a8/mutf8-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6914897b0b682322ef85df9ded02869207174f4ae58434b060c58e7da9adb913", size = 25214, upload-time = "2026-07-10T18:23:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/66/51/9ea7a069459be457278cc0355c3b45ca30b4b9b930aa3095c48e8d3e4a76/mutf8-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e208659e973213c24bd78f84787adb23c0eb8beaceef77911e006a59a3bebce3", size = 24840, upload-time = "2026-07-10T18:23:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/46/9a/8231b544b67a0c9383ab1e5c418526ed321eec1c432fab6aefbea7366944/mutf8-1.1.0-cp313-cp313-win32.whl", hash = "sha256:85ad5bb6c40ba10d18d7bf407c13a25f9fadeb27641fc691e04529609cdbcbd5", size = 14500, upload-time = "2026-07-10T18:23:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/a9ee3bf536f45c7ab5d144fc240abc215c683d88e6fdbb63edd0bb64ef35/mutf8-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:867270fa62a507c2a80591d5f444d96f8739507cb4a135b03319e37ca727f3c7", size = 14893, upload-time = "2026-07-10T18:23:13.418Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/8114ed0298fd2d90a6a9fe24ff8b2519db800c9c8523af2b5a8b39fddb53/mutf8-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:81dcc89c127bf4bf7453c9790032ab2fd2881a5edd6bf464d9e737e7d8a9dcdb", size = 13514, upload-time = "2026-07-10T18:23:14.389Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/ff71996313f10fac965c99c1b2780d92105cae5ed349b4aa30d94a6b37a7/mutf8-1.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:31e8c701128647f8795054e30b0e2f1bb0bcd6e89ab50f0cbf9803413adf98d9", size = 24937, upload-time = "2026-07-10T18:23:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/3b/14/30f26bbe63c0206206d06b7ab65a44ddb62408050670b01ef35a4df5693c/mutf8-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d51b2297b9e0908d21ee41962305c0552d918bcbcb5733c018e98afc4c5d0da4", size = 25871, upload-time = "2026-07-10T18:23:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/11b0b4bff9d7cfc585c9d6974e2d4846dc50c722294a06c1c0287bbac3b1/mutf8-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:42abec0a8f96b03d27edd01a9e18fc375fd1be3166d8fd03b164f5b3cab0abd4", size = 25276, upload-time = "2026-07-10T18:23:17.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6c/5541bc1d05c153ab617dbe7e2620db01653cb4dcab753c76cea6fce59283/mutf8-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ea196f3a49186a3a28e583244fe893d11f1077285b939fcfecc3b5eaf58882eb", size = 24854, upload-time = "2026-07-10T18:23:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/ecf865df12af83033a585bee2c750ba7f8e919e5c5d76af3e1f91759aa37/mutf8-1.1.0-cp314-cp314-win32.whl", hash = "sha256:e7801dbdd5f22763f21558e817804f9eb0c8fc94f7ea238c8f5380c2e1b23b62", size = 14692, upload-time = "2026-07-10T18:23:19.124Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3d/3afe3481fe99959a0dc0e7122d8114d50c044e227d4693ed366d96faf2ca/mutf8-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:2696f757d799fa6f43584cc201fe8bf7a882de13459c5522b169c91ce4e09560", size = 15122, upload-time = "2026-07-10T18:23:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/e7b9b40db43806ab5cdac20deb705f08b874a6af0ec7d4614e191334c368/mutf8-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3364bd2f0b302be14d4d7e2ab4617bf81e99bcbd16d36cc135b344bb2aa46f84", size = 13644, upload-time = "2026-07-10T18:23:20.9Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/04886543e51cf665f7cabaf1f9fed0988141c0cce9eb499ad9b60c9b3b03/mutf8-1.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63bdc07d838c05a5579ab00222192cf573057364e89471f8e64fbf453cfe8145", size = 25452, upload-time = "2026-07-10T18:23:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3f/c3ba1c00f8ec49c29e77ce49e061cc1058f8c48d9ec74b7795361a3b2b0e/mutf8-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e28f11e5b8ab759fe662bad8c6cbbdab7420639613725b6875cc2c73995032d0", size = 26443, upload-time = "2026-07-10T18:23:22.809Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/501f5b35728ede5b74460401c3c281ec54dfdd4acefd682aa7362d0575fb/mutf8-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7c257c6c88821c067a167accde6f5b4af6440d641d993a3a557e80314c076f4d", size = 25819, upload-time = "2026-07-10T18:23:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8e/f5fc3a213ed3ebe5d8912cd6ebc29c735661237a51226839d4d0aabc1fae/mutf8-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c4ef2a7c2c807973e4e5c063cb5459f3979efe8f86f9e71eccdd86fc77b306e", size = 25397, upload-time = "2026-07-10T18:23:24.933Z" }, + { url = "https://files.pythonhosted.org/packages/29/fc/f75fefacb5cb7d42170628422827202770386ad98e27a0f2b59df1c605c8/mutf8-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3ba488375e86d185c187b39d90f8894b996128f721e8ff3eae72dbf6c66d92e8", size = 14828, upload-time = "2026-07-10T18:23:25.902Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7f/8505e1bfa0fa6110f5aeef3a5e62bb234862da3c18ad129fb8e499116ae7/mutf8-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a441d6542d874062e8223ef868195940f281051f4eba82c2d420de12bb133415", size = 15272, upload-time = "2026-07-10T18:23:26.951Z" }, +] + [[package]] name = "networkx" version = "3.4.2" @@ -745,6 +1395,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + [[package]] name = "pefile" version = "2024.8.26" @@ -754,6 +1413,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, ] +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + [[package]] name = "platformdirs" version = "4.10.0" @@ -784,6 +1455,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/2d/d741fbbbcba7eb6f9f1a829373c558114d59cb882b95f941aa0dc060861f/plumbum-2.0.1-py3-none-any.whl", hash = "sha256:27a454980f91689aae8f18242a36daaf2636219171cf0e6a849744aa1d6fff85", size = 164460, upload-time = "2026-06-08T14:44:04.75Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.53" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, +] + [[package]] name = "protobuf" version = "7.35.1" @@ -827,6 +1510,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/9f/abfd2959e9261dd5217ca8551d4de211ca6ab26fe9b72cf44731ff6c4442/pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1", size = 20563, upload-time = "2026-09-10T21:41:22.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/18/83376915176eb058cb86470eb7396388a13350b09a8f233a79303bbcbc5b/pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a", size = 11893, upload-time = "2026-09-10T21:41:21.532Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -915,6 +1616,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/80/37ffc60afbf56bbc8eacbdd1956a69032b121a8355552a22ceb823a1e550/pydemumble-0.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:04e7c5bd6750dcfb899c5098faa204c2db98c7affdb47b1b9e490fb4e4b7cf07", size = 171299, upload-time = "2025-02-15T05:19:09.494Z" }, ] +[[package]] +name = "pydot" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, +] + [[package]] name = "pyelftools" version = "0.33" @@ -933,6 +1646,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pypcode" version = "3.3.3" @@ -1023,8 +1745,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "bitstring", marker = "python_full_version < '3.12'" }, - { name = "cffi", marker = "python_full_version < '3.12' and implementation_name == 'cpython'" }, + { name = "bitstring" }, + { name = "cffi", marker = "implementation_name == 'cpython'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/d5/4df75aa08d1759bc67df8d3c1e1828605d38d2185dea81baff805c8a8742/pyvex-9.2.213.tar.gz", hash = "sha256:772a3588b5fdff1ca3b57379b2906a2f1ca1ee68290cd9c75a730183245d38fc", size = 3649139, upload-time = "2026-04-28T18:04:59.109Z" } wheels = [ @@ -1058,8 +1780,8 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "bitstring", marker = "python_full_version >= '3.12'" }, - { name = "cffi", marker = "python_full_version >= '3.12' and implementation_name == 'cpython'" }, + { name = "bitstring" }, + { name = "cffi", marker = "implementation_name == 'cpython'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/9e/59100c676aad44c39537d4ce5ec0fd753211b9b82a03ea3bb9ee4130c0b4/pyvex-9.2.221.tar.gz", hash = "sha256:f33a02903eafa7da4bf6cafa804cd2bb8707602eea8e9e445642590dd84bf1e2", size = 3649005, upload-time = "2026-06-03T23:38:49.23Z" } wheels = [ @@ -1114,6 +1836,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/ea/34d31e0e4484d2acdd25a24d56a26be7855f9119244fd9ea16d00095b01e/pyxdia-0.1.0-py3-none-win_amd64.whl", hash = "sha256:5bd17cec300f53d0bb7926f64095e3806674c6a82f8c2a5c6be24aa6fc1384a1", size = 958051, upload-time = "2025-06-13T20:03:42.789Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "r2pipe" version = "1.9.8" @@ -1172,6 +1958,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/271aa905cf2964f841371a97f3e63ab692bf51b4423d0491e67bc7f64037/sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b", size = 9969559, upload-time = "2026-09-15T21:06:57.337Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/2f/7a427d7e10bc5f285d41fa1cadf46031ffc6c84f35a9d6e19737587a4eb0/sqlalchemy-2.0.54-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24ae093dec196ba37fc2beb0316de53e7871d3d246a50faecbbb53034e41ded2", size = 2189267, upload-time = "2026-09-15T22:11:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/80122de187c0f80cb47addce97609160004a8605010afdd815e608fcf003/sqlalchemy-2.0.54-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cc6532f930c27974e9239e5ce5abebe7600ba9807cea4fcf42f1b6cab18fe7", size = 3358362, upload-time = "2026-09-15T22:33:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/c8/93/9d26b109f5472017bf6e7a1b9dac12ac5f6165ba37a297834d0690c42cf3/sqlalchemy-2.0.54-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e7a76d5dce712ce50435d0f97181eb955ec27d138c004176f01282e063bac52", size = 3357692, upload-time = "2026-09-15T22:33:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/63e800b45bd4fa59cb4b14c013130e0aa92c7a019a0b8bf68115643bf32b/sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f5c09090b1a7c4d389d1431f820931e8df318f82caafc53f9a72c872fef467c5", size = 3308258, upload-time = "2026-09-15T22:33:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fe/6c96fe018b9db15476b46619228dc0749cdd9b4dc8baab89aca59205b605/sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:762cfe4d340c56368256d936a98b620a9a5650e49c1c84eba51d6edd17ffefb2", size = 3336734, upload-time = "2026-09-15T22:33:16.533Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f1/8d225d6b77863ae83ba51e913fe5954bcf58b49dd54229f0b5ba0bc983f0/sqlalchemy-2.0.54-cp310-cp310-win32.whl", hash = "sha256:6b6d4e601c4f6d85e99bb3416107cc9418c5603ca73d4ee0f5f8d79c2a1ed9e8", size = 2144294, upload-time = "2026-09-15T22:41:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f6/4247fdd087bf1b39be90930bb2b276a1c55041540628c8736512b4242669/sqlalchemy-2.0.54-cp310-cp310-win_amd64.whl", hash = "sha256:03cbf8d9a67da618bd65500a5eb3ddac89caf4c61e99b2f03fa4a1952a0725a9", size = 2170318, upload-time = "2026-09-15T22:41:21.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/bf/7339b18ef05c03335a32f077e450c8040eab0c952c2964e32cd889eafc22/sqlalchemy-2.0.54-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d03084f3352dd92048cb19c71d90f116d076c9c7937e0ebc7752c4685de6d38", size = 2187776, upload-time = "2026-09-15T22:32:35.535Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bb/3b6f5f3f51582d63af4c88b1e70da7888e936dee6b86a237b7633fb3f216/sqlalchemy-2.0.54-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92622fbbda1b1fe1632f3402a6e516a93c0e41d9158839c6b3dfb12117f26b72", size = 3428952, upload-time = "2026-09-15T22:40:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/ffd6b59795ebec81d582ad866f71ec7c73d356768e0442c6a07bba61c40c/sqlalchemy-2.0.54-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5800ddea045c2c860ef1d359a07a3066c7c0c426f45e3abc3874e116cb3c6937", size = 3428566, upload-time = "2026-09-15T22:35:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/63/ff/1f0bffb0653b30711e2445a48968eff9412b4e5bf0870f1e72e8a69595ef/sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019abef05a4b5eafc8eae6fb483167fa28a4dbe5f518d577b744f31a5276a37", size = 3377697, upload-time = "2026-09-15T22:40:16.494Z" }, + { url = "https://files.pythonhosted.org/packages/97/76/774906be0d41cc4a14ecfbc015112fb7d0a3f1c3393f2c179d10388f8f0b/sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b67749f7da3985a529cefbb1474783cb91ef44371cb9713630bade3de908760d", size = 3402966, upload-time = "2026-09-15T22:35:35.726Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bb/1d7fe50ace612bf342d3440d8b09544746949120326c3dc487cfc2592fba/sqlalchemy-2.0.54-cp311-cp311-win32.whl", hash = "sha256:2f61a70b3b82e2ec7ad6a4f2301422b9ca93ff06917983e41317bcae878bddf6", size = 2142297, upload-time = "2026-09-15T21:25:19.917Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/18fe80f64b1f6bcd422e649e40a78031f8624b2fde22ca54e950dfa6e25a/sqlalchemy-2.0.54-cp311-cp311-win_amd64.whl", hash = "sha256:1d887fbd5d248e250807bd801e697fc73e3b44866ce5f093dbc90512e75bde25", size = 2169222, upload-time = "2026-09-15T21:25:21.114Z" }, + { url = "https://files.pythonhosted.org/packages/c6/30/75504fd1d70458000e85a3e772333dd0fbf80254b0dc4d41c99922e4d112/sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2", size = 2187596, upload-time = "2026-09-15T22:32:37.031Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3d/5dfbb9528a391186a99986daecc5cbe003f34408dee91db8f5cdcf917040/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b", size = 3448745, upload-time = "2026-09-15T22:40:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19", size = 3466644, upload-time = "2026-09-15T22:35:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/66/68/4beab40ae60ac3d679dbbb46bc0d2bb277013264a0bde37ecaf4b6780e98/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5", size = 3401519, upload-time = "2026-09-15T22:40:19.907Z" }, + { url = "https://files.pythonhosted.org/packages/e5/df/a24757e3249b1c7c1c5f0317666d16a8ab901ba239819ecef29eb4ed172e/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b", size = 3432590, upload-time = "2026-09-15T22:35:39.434Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0c/69559e3d90d200fbfe9c44aff39a9ff4e506b1a3e9557a203d69df721244/sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e", size = 2140825, upload-time = "2026-09-15T21:25:22.444Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/4a0f7113664c2877906db143709c52ddbd28a88e4d51f0b520bc30048ba7/sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf", size = 2170005, upload-time = "2026-09-15T21:25:23.885Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/021ccd7a838425f52e67c02edc541e8d53be8a27a103e684e14b18aac17b/sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b", size = 2184259, upload-time = "2026-09-15T22:28:59.153Z" }, + { url = "https://files.pythonhosted.org/packages/74/8f/f95de908a4af7ac3a6925cdcb837a93274f608925a0fbc786df38811bccf/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4", size = 3398792, upload-time = "2026-09-15T22:29:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/84/26/bd327a1a6be223e438c98ac8faf299307d83797225a81f3b209607a9fd98/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518", size = 3417562, upload-time = "2026-09-15T22:40:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b1/f13a8fe8e8e167d8e65484b91b827897b8a5ca1efd7ba6e87a5f63412fed/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae", size = 3349138, upload-time = "2026-09-15T22:29:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/5fd58f03281a74c57f02f509433e1be58a79d9f5b0e14020e665e0dcb614/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758", size = 3382943, upload-time = "2026-09-15T22:40:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/4b/73/e29fa88dfc809857a55e6523911d345d00cab4adf87032c21ade8874223c/sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c", size = 2138931, upload-time = "2026-09-15T22:42:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/89/9d/885e7491836f3dad368c74f0d5b93551d37b0ec66c9e8753240ee988308f/sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747", size = 2166496, upload-time = "2026-09-15T22:42:53.603Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c0/4a6503c9d22d6d00a5631082ab1484222ecf7d573db791e0f53161bf7745/sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0", size = 2185899, upload-time = "2026-09-15T22:29:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/f4424f618bd1f373761a32a821d53ce2c257350e9894bae9b968cb03d8fd/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139", size = 3394763, upload-time = "2026-09-15T22:29:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/59/d2/7f0c77f8e042cb5f28275fea29c3080b4ac6fd4b3fdd7f59ff1ef3e28c11/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e", size = 3402800, upload-time = "2026-09-15T22:40:36.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/32/3eaa930bcf71d17a72587081d2706a5fb97ab3f11a7e0fb838f581f7cff1/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912", size = 3341469, upload-time = "2026-09-15T22:29:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/eb/cc/cddb6cbd4408e5c55b3bf722be26b9d3b54d42509f901b7ecc15debd3d1f/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615", size = 3373232, upload-time = "2026-09-15T22:40:39.181Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/9c2aa5efc642b7f3b985d13565cd1a5e78856e079ef3022796fea5180498/sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44", size = 2142018, upload-time = "2026-09-15T22:42:55.118Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/3594f1f51769feb3022d686135dc5d8682a12345ed15ae61d0c0ca42cbee/sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835", size = 2169220, upload-time = "2026-09-15T22:42:56.727Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a5/c211a9a7af83222509519407e16a4db760c6df3d03be69ebc5414d465321/sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a", size = 2208458, upload-time = "2026-09-15T22:30:05.718Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2e/490ad7b3731116cb48ba170f7722eaa99a89707193e54389ca84b7ad55af/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77", size = 3660585, upload-time = "2026-09-15T22:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/eb/25/15dfe6814847eeda773bd58ab6cf42a94b0176e5cf25a578fc1165777160/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd", size = 3624442, upload-time = "2026-09-15T22:36:38.377Z" }, + { url = "https://files.pythonhosted.org/packages/aa/19/724d0a6a2fb2a86ff2d6008e581c258f722d9b7d8e52adc7b79085febdd4/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f", size = 3562972, upload-time = "2026-09-15T22:36:08.581Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/9df1bd81c2f2d000cf5e7a1b1a9b331468a3aab939ad983355e701fa42b2/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb", size = 3576479, upload-time = "2026-09-15T22:36:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/b5775465d3b89061d7c46057c31c56ff8fb6c509550b2b0c6570ffc248b3/sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51", size = 2174794, upload-time = "2026-09-15T22:31:45.925Z" }, + { url = "https://files.pythonhosted.org/packages/77/f8/296c2e46b4ccd3f29b00b954ef2f195dde32f98e352ed21de1d292cedc0d/sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970", size = 2211942, upload-time = "2026-09-15T22:31:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/24/a1/bd5e3e99bc9c8863b51ac5b9b03008a7f2da8c6b59695992f5c654e1265b/sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2", size = 1958015, upload-time = "2026-09-15T22:24:22.95Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1227,6 +2082,7 @@ name = "tocode-cli" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "droidasc" }, { name = "ida-domain" }, { name = "idapro" }, { name = "r2pipe" }, @@ -1246,6 +2102,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "angr", marker = "extra == 'angr'", specifier = ">=9.2" }, + { name = "droidasc", specifier = "==0.1.1.post1" }, { name = "ida-domain", specifier = "==0.5.0" }, { name = "idapro", specifier = "==0.0.9" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.2" }, @@ -1321,6 +2178,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "traitlets" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1335,7 +2201,7 @@ name = "uefi-firmware" version = "1.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future", marker = "python_full_version < '3.12'" }, + { name = "future" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/d2/ee12d6dac85e403779ab4de6904dad2053502d0a2c96a962255e9e40734f/uefi_firmware-1.16.tar.gz", hash = "sha256:1626b9930b1006f9ec10b75ef889ffc17c632e0b43a4aba07a1090d9023133ea", size = 212873, upload-time = "2026-06-04T14:31:34.606Z" } wheels = [ @@ -1365,6 +2231,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/77/e31b46ae9d3ad1afbb23a4708f804bc7257740a4b54e5e6574e5c59b2c37/uefi_firmware-1.16-cp314-cp314t-win_arm64.whl", hash = "sha256:32a5f01c3fc07c6223fe47e5909bce7dc4ec8a6750af969074c1e0b86713bde0", size = 202380, upload-time = "2026-06-04T14:31:33.188Z" }, ] +[[package]] +name = "wcwidth" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/7a/f98d4ada7c499565ab0c0fcef28a4e54fafa72b8228a6309803c80493c92/wcwidth-0.8.4.tar.gz", hash = "sha256:2dae09efa25253ae2874188e86d6861af3b1652aef4118cdf3f0bda288a957fb", size = 414609, upload-time = "2026-09-17T23:31:45.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/cc/9c0adc5861020de426ec71716b6c1bef8d90694b7d6d0a16202cdb1647f5/wcwidth-0.8.4-py3-none-any.whl", hash = "sha256:2097bb1d28a0ba8fe177c2eb607317f9b1627b03f33ebebfd3da670b337a65ba", size = 299474, upload-time = "2026-09-17T23:31:43.816Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + [[package]] name = "z3-solver" version = "4.13.0.0"