From 7795b38462e93cd5c66e8b1cbd233e50644e0b5f Mon Sep 17 00:00:00 2001 From: The Butcher Date: Mon, 22 Jun 2026 06:11:25 -0400 Subject: [PATCH] feat(android): make lean Termux installs optional-native safe Move native-heavy optional capabilities out of the base install path, add an Android/Termux bootstrap planner and wizard, and document the verified lean install flow. Keep Playwright, PyPI ripgrep, Pillow, tiktoken, and regex detached from the base install so Android can install Code Puppy after the required Termux uv/rust/clang prerequisites are present. --- .github/workflows/ci.yml | 20 +- .github/workflows/publish.yml | 20 +- README.md | 67 +++++ code_puppy/bootstrap.py | 153 +++++++++++ code_puppy/bootstrap_profiles.py | 276 ++++++++++++++++++++ code_puppy/bootstrap_wizard.py | 259 ++++++++++++++++++ code_puppy/tools/browser/browser_manager.py | 50 +++- code_puppy/tools/image_tools.py | 18 +- docs/ANDROID_INSTALL.md | 160 ++++++++++++ pyproject.toml | 9 +- tests/test_bootstrap.py | 167 ++++++++++++ tests/test_bootstrap_wizard.py | 156 +++++++++++ tests/test_browser_optional.py | 45 ++++ uv.lock | 186 ++----------- 14 files changed, 1405 insertions(+), 181 deletions(-) create mode 100644 code_puppy/bootstrap.py create mode 100644 code_puppy/bootstrap_profiles.py create mode 100644 code_puppy/bootstrap_wizard.py create mode 100644 docs/ANDROID_INSTALL.md create mode 100644 tests/test_bootstrap.py create mode 100644 tests/test_bootstrap_wizard.py create mode 100644 tests/test_browser_optional.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efcc3a998..ccea311c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,27 @@ jobs: - name: Setup uv virtual environment run: uv venv - - name: Install dependencies + - name: Install lean dependencies run: uv pip install -e . + - name: Verify lean install keeps native extras optional + run: | + uv run python - <<'PY' + import importlib.util + + optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"] + installed = [name for name in optional_modules if importlib.util.find_spec(name)] + if installed: + raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}") + print("Lean install optional-native smoke check passed") + PY + + - name: Install CI test extras + # The full historical test suite exercises image/search behavior. + # macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses + # system rg from `pkg install ripgrep` instead. + run: uv pip install -e ".[images,search]" + - name: Install pexpect for integration tests run: uv pip install pexpect>=4.9.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 07b100d2a..bc26a8cde 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,11 +34,29 @@ jobs: - name: Setup uv virtual environment run: uv venv - - name: Install dependencies + - name: Install lean durable dependencies # [durable] extras pulls in dbos, required by integration test # tests/integration/test_dbos_enabled.py run: uv pip install -e ".[durable]" + - name: Verify lean install keeps native extras optional + run: | + uv run python - <<'PY' + import importlib.util + + optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"] + installed = [name for name in optional_modules if importlib.util.find_spec(name)] + if installed: + raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}") + print("Lean install optional-native smoke check passed") + PY + + - name: Install CI test extras + # The full historical test suite exercises image/search behavior. + # macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses + # system rg from `pkg install ripgrep` instead. + run: uv pip install -e ".[durable,images,search]" + - name: Install pexpect for integration tests run: uv pip install pexpect>=4.9.0 diff --git a/README.md b/README.md index acc9cb7ae..c7f4e1141 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,73 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie uvx code-puppy ``` +#### Android / Termux + +Code Puppy installs lean on Android/Termux. Read the full guide at +[`docs/ANDROID_INSTALL.md`](docs/ANDROID_INSTALL.md). Android's Python wheel +availability is different from desktop Linux, so the installer keeps optional +native-heavy features detached and makes the required Termux build toolchain +explicit. + +**The one-command path (recommended):** the interactive bootstrap *wizard* +detects the device and walks you through every remaining step (native helper +packages, Code Puppy) with a `[Y/n]` confirmation before each one, then verifies +the install. Install `uv rust clang` first because `uvx --from code-puppy ...` +must resolve Code Puppy's required Python dependencies before the wizard can run: + +```bash +pkg update +pkg install python git uv rust clang +uvx --from code-puppy code-puppy-bootstrap wizard +``` + +The wizard is stdlib-only, gates every state-changing step behind a prompt, and +finishes with a verification + reconciliation summary. Add `--yes` to +auto-confirm for scripted installs, or `--dry-run` to preview without executing +anything. You can also inspect the plan without running it: + +```bash +uvx --from code-puppy code-puppy-bootstrap detect # what the wizard sees +uvx --from code-puppy code-puppy-bootstrap plan # the lean install plan +``` + +
+Prefer to drive it by hand? Here is the manual flow the wizard runs. + +```bash +# In Termux +pkg update +pkg install python git uv rust clang ripgrep proot +uv tool install --refresh code-puppy +code-puppy -i +``` + +
+ +Install `ripgrep` from Termux (`pkg install ripgrep`) so the file-search and +recursive-listing tools work — Code Puppy uses the system `rg` binary. Android +support does not remove search support; it only prevents the PyPI `ripgrep` +package from being installed on Android/Termux, where it is known to fail. +Desktop and CI installs using the optional `[search]` extra remain unchanged. + +#### Optional: Browser automation + +Browser tools are powered by [Playwright](https://playwright.dev/), which has no +wheels for some platforms (e.g. Android/Termux). It is therefore an optional +extra and is **not** part of the base install. On supported platforms, opt in: + +```bash +pip install "code-puppy[browser]" +# or +uv tool install "code-puppy[browser]" + +# then download the browser binaries +playwright install chromium +``` + +Without this extra, Code Puppy runs normally; browser tools simply report that +the optional `browser` dependency is missing when they are invoked. + #### Optional: DBOS durable execution Code Puppy ships with an optional [DBOS](https://github.com/dbos-inc/dbos-transact-py)-backed diff --git a/code_puppy/bootstrap.py b/code_puppy/bootstrap.py new file mode 100644 index 000000000..3c71510d1 --- /dev/null +++ b/code_puppy/bootstrap.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any + +from code_puppy.bootstrap_profiles import ( + available_profiles, + build_install_plan, + detect_environment, +) + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Code Puppy bootstrap planner for lean environment-aware installs" + ) + subparsers = parser.add_subparsers(dest="command") + + detect_parser = subparsers.add_parser( + "detect", + help="Inspect the current environment without importing the runtime CLI", + ) + detect_parser.add_argument("--json", action="store_true", help="Print JSON output") + + plan_parser = subparsers.add_parser( + "plan", + help="Build a lean install/reattach plan from a builtin profile and optional manifest", + ) + plan_parser.add_argument( + "--profile", + default="auto", + choices=["auto", *available_profiles()], + help="Builtin install profile to use", + ) + plan_parser.add_argument( + "--manifest-json", + default="", + help="Inline JSON manifest with extras_add/extras_remove/notes overrides", + ) + plan_parser.add_argument( + "--manifest-file", + default="", + help="Path to a JSON manifest file with plan overrides", + ) + plan_parser.add_argument("--json", action="store_true", help="Print JSON output") + + wizard_parser = subparsers.add_parser( + "wizard", + help="Interactively install Code Puppy step-by-step with confirmation", + ) + wizard_parser.add_argument( + "--profile", + default="auto", + choices=["auto", *available_profiles()], + help="Builtin install profile to use", + ) + wizard_parser.add_argument( + "--yes", + action="store_true", + help="Auto-confirm every step (non-interactive automation)", + ) + wizard_parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would run without executing anything", + ) + + return parser + + +def _human_detect(environment: dict[str, Any]) -> str: + lines = [ + "Code Puppy bootstrap environment", + f"- python: {environment['python_executable']}", + f"- version: {environment['python_version']}", + f"- platform: {environment['platform_system']} {environment['platform_release']} ({environment['platform_machine']})", + f"- termux: {'yes' if environment['is_termux'] else 'no'}", + f"- android: {'yes' if environment['is_android'] else 'no'}", + f"- uv: {'yes' if environment['has_uv'] else 'no'}", + f"- uvx: {'yes' if environment['has_uvx'] else 'no'}", + f"- proot: {'yes' if environment['has_proot'] else 'no'}", + f"- ripgrep: {'yes' if environment['has_ripgrep'] else 'no'}", + f"- rust: {'yes' if environment['has_rust'] else 'no'}", + f"- clang: {'yes' if environment['has_clang'] else 'no'}", + ] + return "\n".join(lines) + + +def _human_plan(plan: dict[str, Any]) -> str: + lines = [ + f"Profile: {plan['profile']}", + f"Package spec: {plan['package_spec']}", + f"Install: {plan['install_command']}", + f"Reattach: {plan['reattach_command']}", + f"Run: {plan['run_command']}", + ] + if plan["extras"]: + lines.append(f"Extras: {', '.join(plan['extras'])}") + if plan["degraded_capabilities"]: + lines.append("Degraded until reattach:") + lines.extend(f"- {item}" for item in plan["degraded_capabilities"]) + if plan["system_packages"]: + lines.append("Suggested system packages:") + lines.extend(f"- {item}" for item in plan["system_packages"]) + if plan["notes"]: + lines.append("Notes:") + lines.extend(f"- {item}" for item in plan["notes"]) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + command = args.command or "plan" + + try: + if command == "wizard": + from code_puppy.bootstrap_wizard import run_wizard + + return run_wizard( + profile=getattr(args, "profile", "auto"), + assume_yes=getattr(args, "yes", False), + dry_run=getattr(args, "dry_run", False), + ) + + if command == "detect": + environment = detect_environment() + if args.json: + print(json.dumps(environment, indent=2, sort_keys=True)) + else: + print(_human_detect(environment)) + return 0 + + plan = build_install_plan( + requested_profile=getattr(args, "profile", "auto"), + manifest_json=getattr(args, "manifest_json", ""), + manifest_file=getattr(args, "manifest_file", ""), + ) + if getattr(args, "json", False): + print(json.dumps(plan, indent=2, sort_keys=True)) + else: + print(_human_plan(plan)) + return 0 + except ValueError as exc: + parser.exit(status=2, message=f"error: {exc}\n") + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/code_puppy/bootstrap_profiles.py b/code_puppy/bootstrap_profiles.py new file mode 100644 index 000000000..09f3aef7b --- /dev/null +++ b/code_puppy/bootstrap_profiles.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +import json +import os +import platform +import shlex +import shutil +import sys +from copy import deepcopy +from pathlib import Path +from typing import Any + +DEFAULT_PACKAGE_NAME = "code-puppy" + +_BUILTIN_PROFILES: dict[str, dict[str, Any]] = { + "core": { + "description": "Lean base runtime with no optional extras attached.", + "extras": [], + "notes": [ + "Use this when a launcher only needs the core CLI/runtime.", + "Optional capabilities should reattach later based on deployment policy.", + ], + "system_packages": [], + }, + "android-termux-lean": { + "description": "Android/Termux-safe profile that keeps Python deps lean.", + "extras": [], + "notes": [ + "Install the Termux build toolchain first; pydantic-core has no Android wheel.", + "Avoid browser/image/search extras during initial attach.", + "Reattach optional capabilities only after the target environment is known.", + ], + "system_packages": [ + "pkg install rust clang", + "pkg install ripgrep proot", + ], + }, + "desktop-browser": { + "description": "Desktop-oriented profile with local browser, image, and search helpers.", + "extras": ["browser", "images", "search"], + "notes": [ + "Useful for developer laptops and desktop automation environments.", + ], + "system_packages": [], + }, + "developer-full": { + "description": "Broad developer profile with provider and tooling extras attached.", + "extras": [ + "bedrock", + "browser", + "durable", + "images", + "search", + ], + "notes": [ + "This is intentionally heavy; use it only when the environment is known-good.", + ], + "system_packages": [], + }, +} + + +def available_profiles() -> list[str]: + return sorted(_BUILTIN_PROFILES) + + +def detect_environment() -> dict[str, Any]: + executable = Path(sys.executable).expanduser().resolve() + release = platform.release() + system_name = platform.system() + termux_prefix = os.environ.get("PREFIX", "") + termux_version = os.environ.get("TERMUX_VERSION", "") + is_termux = ( + bool(termux_version) + or "com.termux" in str(executable) + or (system_name == "Linux" and "com.termux" in termux_prefix) + ) + is_android = is_termux or "android" in release.lower() + + return { + "python_executable": str(executable), + "python_version": platform.python_version(), + "platform_system": system_name, + "platform_release": release, + "platform_machine": platform.machine(), + "is_android": is_android, + "is_termux": is_termux, + "is_windows": system_name == "Windows", + "is_macos": system_name == "Darwin", + "is_linux": system_name == "Linux", + "has_uv": shutil.which("uv") is not None, + "has_uvx": shutil.which("uvx") is not None, + "has_proot": shutil.which("proot") is not None, + "has_ripgrep": shutil.which("rg") is not None, + "has_rust": shutil.which("rustc") is not None + and shutil.which("cargo") is not None, + "has_clang": shutil.which("clang") is not None, + "has_git": shutil.which("git") is not None, + } + + +def auto_profile_name(environment: dict[str, Any]) -> str: + if environment.get("is_termux") or environment.get("is_android"): + return "android-termux-lean" + if environment.get("is_windows") or environment.get("is_macos"): + return "desktop-browser" + if environment.get("is_linux"): + return "desktop-browser" + return "core" + + +def resolve_profile_name( + requested_profile: str | None, environment: dict[str, Any] +) -> str: + raw = ( + requested_profile or os.environ.get("CODE_PUPPY_INSTALL_PROFILE", "auto") + ).strip() + if not raw or raw == "auto": + return auto_profile_name(environment) + if raw not in _BUILTIN_PROFILES: + choices = ", ".join(available_profiles()) + raise ValueError(f"unknown install profile '{raw}'. Choices: auto, {choices}") + return raw + + +def _parse_manifest_json(raw: str) -> dict[str, Any]: + data = json.loads(raw) + if not isinstance(data, dict): + raise ValueError("bootstrap manifest must be a JSON object") + return data + + +def load_manifest( + *, + manifest_json: str = "", + manifest_file: str = "", +) -> dict[str, Any]: + raw_json = ( + manifest_json.strip() + or os.environ.get("CODE_PUPPY_BOOTSTRAP_MANIFEST_JSON", "").strip() + ) + raw_file = ( + manifest_file.strip() + or os.environ.get("CODE_PUPPY_BOOTSTRAP_MANIFEST_FILE", "").strip() + ) + + if raw_json and raw_file: + raise ValueError("choose manifest_json or manifest_file, not both") + if raw_json: + return _parse_manifest_json(raw_json) + if raw_file: + return _parse_manifest_json( + Path(raw_file).expanduser().read_text(encoding="utf-8") + ) + return {} + + +def _normalize_list(value: Any, *, field_name: str) -> list[str]: + if value in (None, ""): + return [] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError( + f"bootstrap manifest field '{field_name}' must be a list of strings" + ) + return [item.strip() for item in value if item.strip()] + + +def merge_profile( + profile_name: str, + manifest: dict[str, Any] | None = None, +) -> tuple[dict[str, Any], list[str]]: + profile = deepcopy(_BUILTIN_PROFILES[profile_name]) + reasons = [f"base profile: {profile_name}"] + manifest = manifest or {} + if not manifest: + return profile, reasons + + profile_override = manifest.get("profile", "") + if profile_override and profile_override != profile_name: + raise ValueError( + f"manifest profile '{profile_override}' does not match resolved profile '{profile_name}'" + ) + + extras = set(profile["extras"]) + extras.update(_normalize_list(manifest.get("extras_add"), field_name="extras_add")) + extras.difference_update( + _normalize_list(manifest.get("extras_remove"), field_name="extras_remove") + ) + profile["extras"] = sorted(extras) + + for key in ("notes", "system_packages"): + profile[key] = profile[key] + _normalize_list(manifest.get(key), field_name=key) + + description = manifest.get("description", "") + if description: + if not isinstance(description, str): + raise ValueError("bootstrap manifest field 'description' must be a string") + profile["description"] = description.strip() + + package_name = manifest.get("package_name", "") + if package_name: + if not isinstance(package_name, str): + raise ValueError("bootstrap manifest field 'package_name' must be a string") + profile["package_name"] = package_name.strip() + + if manifest: + reasons.append("manifest overrides applied") + return profile, reasons + + +def package_spec(package_name: str, extras: list[str]) -> str: + if not extras: + return package_name + return f"{package_name}[{','.join(sorted(extras))}]" + + +def install_command(spec: str, environment: dict[str, Any]) -> str: + if environment.get("has_uv"): + return f"uv tool install --refresh {shlex.quote(spec)}" + return f"python -m pip install {shlex.quote(spec)}" + + +def reattach_command(spec: str, environment: dict[str, Any]) -> str: + if environment.get("has_uv"): + return f"uv tool install --refresh {shlex.quote(spec)}" + return f"python -m pip install --upgrade {shlex.quote(spec)}" + + +def build_install_plan( + *, + requested_profile: str | None = None, + manifest_json: str = "", + manifest_file: str = "", +) -> dict[str, Any]: + environment = detect_environment() + resolved_profile = resolve_profile_name(requested_profile, environment) + manifest = load_manifest(manifest_json=manifest_json, manifest_file=manifest_file) + profile, reasons = merge_profile(resolved_profile, manifest) + selected_package = profile.get("package_name", DEFAULT_PACKAGE_NAME) + selected_extras = profile["extras"] + spec = package_spec(selected_package, selected_extras) + + degraded = [] + if resolved_profile == "android-termux-lean": + degraded = [ + "browser automation extras detached", + "image/search extras detached", + ] + + missing_system_packages = [] + if resolved_profile == "android-termux-lean" and not environment.get("has_rust"): + missing_system_packages.append("rust") + if resolved_profile == "android-termux-lean" and not environment.get("has_clang"): + missing_system_packages.append("clang") + if resolved_profile == "android-termux-lean" and not environment.get("has_ripgrep"): + missing_system_packages.append("ripgrep") + if resolved_profile == "android-termux-lean" and not environment.get("has_proot"): + missing_system_packages.append("proot") + + return { + "profile": resolved_profile, + "package_name": selected_package, + "package_spec": spec, + "description": profile["description"], + "extras": selected_extras, + "notes": profile["notes"], + "system_packages": profile["system_packages"], + "missing_system_packages": missing_system_packages, + "environment": environment, + "reasons": reasons, + "install_command": install_command(spec, environment), + "reattach_command": reattach_command(spec, environment), + "run_command": "code-puppy -i", + "degraded_capabilities": degraded, + "manifest_applied": bool(manifest), + } diff --git a/code_puppy/bootstrap_wizard.py b/code_puppy/bootstrap_wizard.py new file mode 100644 index 000000000..bb61991b0 --- /dev/null +++ b/code_puppy/bootstrap_wizard.py @@ -0,0 +1,259 @@ +"""Interactive, environment-aware install wizard for Code Puppy. + +The bootstrap *planner* (``code_puppy.bootstrap_profiles``) decides *what* +should be installed. This module is the *wizard*: it walks an operator -- +especially an Android/Termux newcomer -- through *doing* it, one confirmed +step at a time, then verifies and reconciles the result. + +Design rules honored here: +- stdlib-only (``subprocess``/``shutil``) so it runs before any heavy deps +- destructive/state-altering steps are gated behind explicit confirmation +- ``--dry-run`` never executes; ``--yes`` auto-confirms for automation +- the wizard always ends with a verification + reconciliation summary +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from typing import Any, Callable + +from code_puppy.bootstrap_profiles import build_install_plan, detect_environment + + +@dataclass +class WizardStep: + """A single confirmable unit of install work.""" + + key: str + title: str + command: str + explanation: str + # Returns True when the step's goal is *already* satisfied (skip it). + is_satisfied: Callable[[], bool] = field(default=lambda: False) + required: bool = True + + +@dataclass +class StepOutcome: + key: str + status: str # satisfied | done | skipped | failed | dry-run + detail: str = "" + + +def _has(binary: str) -> bool: + return shutil.which(binary) is not None + + +def build_steps(profile: str | None = None) -> tuple[list[WizardStep], dict[str, Any]]: + """Derive the ordered wizard steps from the canonical install plan. + + Reusing :func:`build_install_plan` keeps a single source of truth for + *what* to install -- the wizard only decides *how* to walk it. + """ + + plan = build_install_plan(requested_profile=profile) + env = plan["environment"] + steps: list[WizardStep] = [] + + # 1. Package manager (uv) -- the engine everything else rides on. + if not env.get("has_uv"): + if env.get("is_termux") or env.get("is_android"): + uv_cmd = "pkg install -y uv" + else: + uv_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh" + steps.append( + WizardStep( + key="uv", + title="Install the uv package manager", + command=uv_cmd, + explanation=( + "uv installs and runs Code Puppy in an isolated tool env; " + "Termux should use its packaged uv instead of building uv from PyPI." + ), + is_satisfied=lambda: _has("uv"), + ) + ) + + # 2. System packages: Android build toolchain + native helper binaries. + missing = plan.get("missing_system_packages") or [] + if missing: + steps.append( + WizardStep( + key="system_packages", + title=f"Install Termux system packages: {', '.join(missing)}", + command=f"pkg install -y {' '.join(missing)}", + explanation=( + "Termux needs rust/clang to build unavoidable native Python " + "deps (pydantic-core), plus helper binaries like ripgrep/proot." + ), + is_satisfied=lambda missing=missing: all( + _has({"ripgrep": "rg", "rust": "rustc"}.get(m, m)) for m in missing + ), + ) + ) + + # 3. Code Puppy itself. + steps.append( + WizardStep( + key="code_puppy", + title=f"Install Code Puppy ({plan['package_spec']})", + command=plan["install_command"], + explanation=plan["description"], + is_satisfied=lambda: False, # always (re)attach; --refresh is idempotent + ) + ) + + return steps, plan + + +def _confirm(prompt: str, *, assume_yes: bool) -> bool: + if assume_yes: + print(f"{prompt} [auto-yes]") + return True + if not sys.stdin.isatty(): + # Non-interactive without --yes: refuse to act, don't hang. + print(f"{prompt} [no TTY -- skipping; pass --yes to auto-run]") + return False + try: + answer = input(f"{prompt} [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + return False + return answer in ("", "y", "yes") + + +def _run_command(command: str) -> tuple[int, str]: + """Run a shell command, streaming nothing, capturing combined output.""" + + try: + proc = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=1800, + ) + except subprocess.TimeoutExpired: + return 124, "timed out after 1800s" + except Exception as exc: # never crash the wizard + return 1, f"failed to launch: {exc}" + output = (proc.stdout or "") + (proc.stderr or "") + return proc.returncode, output.strip() + + +def _verify(plan: dict[str, Any]) -> StepOutcome: + """Confirm Code Puppy actually landed and can answer for itself.""" + + if not _has("code-puppy"): + return StepOutcome( + "verify", + "failed", + "`code-puppy` not found on PATH after install.", + ) + code, out = _run_command("code-puppy --help") + if code != 0: + tail = out.splitlines()[-3:] if out else [] + return StepOutcome("verify", "failed", "; ".join(tail) or f"exit {code}") + return StepOutcome("verify", "done", "`code-puppy --help` runs cleanly.") + + +def run_wizard( + *, + profile: str | None = None, + assume_yes: bool = False, + dry_run: bool = False, +) -> int: + """Execute the interactive install wizard. Returns a process exit code.""" + + steps, plan = build_steps(profile) + env = plan["environment"] + + print("Code Puppy install wizard") + print(f" profile : {plan['profile']}") + print( + " device : " + f"{env['platform_system']} {env['platform_release']} ({env['platform_machine']})" + ) + print(f" python : {env['python_version']}") + print(f" steps : {len(steps)}") + if dry_run: + print(" mode : DRY-RUN (nothing will be executed)") + print() + + outcomes: list[StepOutcome] = [] + + for index, step in enumerate(steps, start=1): + print(f"[{index}/{len(steps)}] {step.title}") + print(f" why: {step.explanation}") + print(f" run: {step.command}") + + if step.is_satisfied(): + print(" -> already satisfied, skipping.\n") + outcomes.append(StepOutcome(step.key, "satisfied")) + continue + + if dry_run: + print(" -> dry-run, not executed.\n") + outcomes.append(StepOutcome(step.key, "dry-run")) + continue + + if not _confirm(" proceed?", assume_yes=assume_yes): + status = "skipped" + print(f" -> {status}.\n") + outcomes.append(StepOutcome(step.key, status)) + if step.required: + print( + " (this step is required -- later steps may fail without it)\n" + ) + continue + + code, out = _run_command(step.command) + if code == 0: + print(" -> done.\n") + outcomes.append(StepOutcome(step.key, "done")) + else: + tail = "\n ".join(out.splitlines()[-5:]) if out else f"exit {code}" + print(f" -> FAILED (exit {code}):\n {tail}\n") + outcomes.append(StepOutcome(step.key, "failed", f"exit {code}")) + if step.required: + print("Required step failed -- stopping. Fix the above and re-run.\n") + _print_summary(outcomes, verify=None) + return 1 + + verify = None + if not dry_run: + verify = _verify(plan) + symbol = "OK" if verify.status == "done" else "FAILED" + print(f"Verification: {symbol} -- {verify.detail}\n") + + _print_summary(outcomes, verify=verify) + + failed = any(o.status == "failed" for o in outcomes) + if verify is not None and verify.status == "failed": + failed = True + return 1 if failed else 0 + + +def _print_summary(outcomes: list[StepOutcome], *, verify: StepOutcome | None) -> None: + print("Summary (state reconciliation):") + for outcome in outcomes: + line = f" - {outcome.key}: {outcome.status}" + if outcome.detail: + line += f" ({outcome.detail})" + print(line) + if verify is not None: + line = f" - verify: {verify.status}" + if verify.detail: + line += f" ({verify.detail})" + print(line) + if verify is not None and verify.status == "done": + print("\nAll set -- run it with: code-puppy -i") + + +def detect_summary() -> dict[str, Any]: + """Thin re-export so callers can pre-flight without importing profiles.""" + + return detect_environment() diff --git a/code_puppy/tools/browser/browser_manager.py b/code_puppy/tools/browser/browser_manager.py index d1b4da07d..ca1b7be08 100644 --- a/code_puppy/tools/browser/browser_manager.py +++ b/code_puppy/tools/browser/browser_manager.py @@ -7,14 +7,58 @@ import atexit import contextvars import os +import sys +import types from pathlib import Path -from typing import Callable, Dict, Optional - -from playwright.async_api import Browser, BrowserContext, Page +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from code_puppy import config from code_puppy.messaging import emit_info, emit_success, emit_warning +if TYPE_CHECKING: + from playwright.async_api import Browser, BrowserContext, Page +else: + Browser = BrowserContext = Page = Any + + +def _missing_playwright(*_args, **_kwargs): + """Raise a friendly error when Playwright is unavailable.""" + raise RuntimeError( + "Playwright is not installed in this environment. " + "Browser tools require the optional 'playwright' dependency " + "(install code-puppy[browser]) on a supported platform." + ) + + +def _install_playwright_stub() -> None: + """Register a minimal stub so browser modules stay importable. + + Keeps optional browser support from crashing startup on platforms where + Playwright wheels are unavailable (e.g. Android/Termux). Browser tools then + fail loudly only when actually invoked, instead of at import time. + """ + playwright_module = sys.modules.get("playwright") + if playwright_module is None: + playwright_module = types.ModuleType("playwright") + sys.modules["playwright"] = playwright_module + + async_api_module = sys.modules.get("playwright.async_api") + if async_api_module is None: + async_api_module = types.ModuleType("playwright.async_api") + async_api_module.Browser = Browser + async_api_module.BrowserContext = BrowserContext + async_api_module.Page = Page + async_api_module.async_playwright = _missing_playwright + sys.modules["playwright.async_api"] = async_api_module + + setattr(playwright_module, "async_api", async_api_module) + + +try: + import playwright.async_api as _playwright_async_api # noqa: F401 +except ImportError: + _install_playwright_stub() + # Registry for custom browser types from plugins (e.g., Camoufox for stealth browsing) _CUSTOM_BROWSER_TYPES: Dict[str, Callable] = {} _BROWSER_TYPES_LOADED: bool = False diff --git a/code_puppy/tools/image_tools.py b/code_puppy/tools/image_tools.py index 9081a09db..57e8a434c 100644 --- a/code_puppy/tools/image_tools.py +++ b/code_puppy/tools/image_tools.py @@ -16,7 +16,18 @@ from pathlib import Path from typing import Any, Dict, Union -from PIL import Image, UnidentifiedImageError +try: + from PIL import Image, UnidentifiedImageError + + PIL_AVAILABLE = True +except ImportError: # Pillow is optional; install code-puppy[images] + Image = None + + class UnidentifiedImageError(Exception): + """Fallback used only when Pillow is not installed.""" + + PIL_AVAILABLE = False + from pydantic_ai import BinaryContent, RunContext, ToolReturn from code_puppy.messaging import emit_error, emit_info, emit_success @@ -40,6 +51,11 @@ def _validate_and_prepare_image( file extension. If the image is resized, the output is normalized to PNG so the returned bytes and MIME type stay in sync like civilized software. """ + if not PIL_AVAILABLE or Image is None: + raise ValueError( + "Image tools require Pillow. Install with: pip install 'code-puppy[images]'" + ) + try: with Image.open(io.BytesIO(image_bytes)) as verified_image: verified_image.verify() diff --git a/docs/ANDROID_INSTALL.md b/docs/ANDROID_INSTALL.md new file mode 100644 index 000000000..aaee3603e --- /dev/null +++ b/docs/ANDROID_INSTALL.md @@ -0,0 +1,160 @@ +# Code Puppy on Android / Termux + +This guide is for a **fresh Termux install on Android**. Code Puppy works on +Android, but it is not a normal desktop Python install: several dependencies use +native code, and Android does not receive the same PyPI wheels as Linux/macOS. + +The installer path below is intentionally conservative: install the Termux build +toolchain first, keep optional heavyweight features detached, then install the +lean Code Puppy runtime. + +## 0. Use the real Termux + +Install Termux from F-Droid or GitHub releases. The Play Store build is stale and +will cause weird package failures. Weird as in "why is my phone haunted?" weird. + +## 1. Recommended: run the bootstrap wizard + +After installing Termux, run: + +```bash +pkg update +pkg install python git uv rust clang +uvx --from code-puppy code-puppy-bootstrap wizard +``` + +Install `uv` from Termux (`pkg install uv`) instead of PyPI. On Android, PyPI may +fall back to building `uv` from source, which needs Rust before you even have the +installer. That is a very silly bootstrapping sandwich, so we skip it. + +The wizard detects your device, explains each step, asks before changing state, +and finishes by verifying `code-puppy --help`. + +For scripted installs: + +```bash +uvx --from code-puppy code-puppy-bootstrap wizard --yes +``` + +To preview only: + +```bash +uvx --from code-puppy code-puppy-bootstrap wizard --dry-run +``` + +## 2. Manual install path + +If you prefer to drive the meat wagon yourself: + +```bash +pkg update +pkg install python git uv rust clang ripgrep proot +uv tool install --refresh code-puppy +code-puppy -i +``` + +### Why `rust clang` are required + +`pydantic-core` is a required dependency of `pydantic`, which Code Puppy uses +through `pydantic-ai`. PyPI does not currently publish Android/Termux wheels for +`pydantic-core`, and Termux does not ship a prebuilt `python-pydantic` package in +the default repo. A fresh Android user therefore needs the Termux Rust/C build +toolchain so pip/uv can build that dependency locally. Install it before running +`uvx --from code-puppy ...`, because `uvx` must install Code Puppy before the +wizard code can execute. + +The base install deliberately avoids other native-heavy optional packages: + +- Playwright is in the `[browser]` extra. +- PyPI's bundled `ripgrep` package is in the `[search]` extra; the Android path + uses Termux's system `rg` from `pkg install ripgrep` instead. +- Pillow is in the `[images]` extra. +- `pydantic-ai-slim[openai]` is intentionally avoided in base because it pulls + `tiktoken` and `regex`, which require additional native builds on Android. + +Android support does not remove search support; it only prevents the PyPI +`ripgrep` package from being installed on Android/Termux, where it is known to +fail. Desktop and CI installs using `[search]` remain unchanged. + +## 3. Optional features + +Install these only after the lean runtime is working. + +```bash +# Image loading / resizing tools +uv tool install --refresh 'code-puppy[images]' + +# From a source checkout / editable install on Android, keep search desktop-only. +# Plain `uv sync` installs dev tools such as Ruff and may try to build them +# from source on Android; use --no-dev for runtime validation. +uv sync --no-dev --python "$(command -v python)" + +# Or, from an activated lean venv: +uv pip install -e '.[images]' + +# Desktop/browser automation environments only; not recommended on native Android +uv tool install --refresh 'code-puppy[browser]' + +# Desktop/CI only: PyPI ripgrep bundle for non-Termux systems. +# Android/Termux should prefer: pkg install ripgrep +uv tool install --refresh 'code-puppy[search]' + +# DBOS-backed durable execution +uv tool install --refresh 'code-puppy[durable]' +``` + +## 4. Verify the install + +```bash +code-puppy --help +code-puppy-bootstrap detect +code-puppy-bootstrap plan --profile auto +``` + +On Android/Termux, the plan should select `android-termux-lean`. + +## Troubleshooting + +### `aarch64-linux-android-clang: No such file or directory` + +Install the compiler: + +```bash +pkg install clang +``` + +### `Rust not found` or `maturin` fails building `pydantic-core` + +Install Rust: + +```bash +pkg install rust +``` + +Then retry: + +```bash +uv tool install --refresh code-puppy +``` + +### `rg` / search tools are unavailable + +Install Termux ripgrep: + +```bash +pkg install ripgrep +``` + +### Image tools say Pillow is missing + +That is expected on the lean Android install. Reattach the optional image extra: + +```bash +uv tool install --refresh 'code-puppy[images]' +``` + +### The install is slow + +Compiling Rust/C dependencies on a phone can take a while. Keep Termux in the +foreground, keep the screen awake if needed, and avoid running it while Android +is aggressively saving battery. diff --git a/pyproject.toml b/pyproject.toml index fb69e0450..47f07b7e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Code generation agent" readme = "README.md" requires-python = ">=3.11,<3.15" dependencies = [ - "pydantic-ai-slim[openai,anthropic,mcp]==1.56.0", + "pydantic-ai-slim[anthropic,mcp]==1.56.0", "typer>=0.12.0", "mcp>=1.9.4", "httpx[http2]>=0.24.1", @@ -22,10 +22,7 @@ dependencies = [ "pyfiglet>=0.8.post1", "rapidfuzz>=3.13.0", "openai>=1.99.1", - "ripgrep==14.1.0", - "playwright>=1.40.0", "termflow-md>=0.1.11", - "Pillow>=10.0.0", "azure-identity>=1.15.0", "anthropic==0.79.0", "boto3>=1.43.9", @@ -53,7 +50,10 @@ classifiers = [ [project.optional-dependencies] bedrock = ["boto3>=1.35.0"] +browser = ["playwright>=1.40.0"] durable = ["dbos>=2.11.0"] +images = ["Pillow>=10.0.0"] +search = ["ripgrep==14.1.0"] [project.urls] repository = "https://github.com/mpfaffenberger/code_puppy" @@ -63,6 +63,7 @@ HomePage = "https://github.com/mpfaffenberger/code_puppy" [project.scripts] code-puppy = "code_puppy.main:main_entry" pup = "code_puppy.main:main_entry" +code-puppy-bootstrap = "code_puppy.bootstrap:main" [tool.logfire] ignore_no_config = true diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py new file mode 100644 index 000000000..840b27c58 --- /dev/null +++ b/tests/test_bootstrap.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from code_puppy import bootstrap, bootstrap_profiles + + +def test_bootstrap_import_does_not_pull_cli_runner(): + result = subprocess.run( + [ + sys.executable, + "-c", + "import sys; import code_puppy.bootstrap; print('code_puppy.cli_runner' in sys.modules)", + ], + capture_output=True, + text=True, + check=True, + ) + assert result.stdout.strip() == "False" + + +def test_detect_environment_termux(monkeypatch): + monkeypatch.setenv("TERMUX_VERSION", "0.119") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + environment = bootstrap_profiles.detect_environment() + assert environment["is_termux"] is True + assert environment["is_android"] is True + + +def test_auto_profile_prefers_android_termux(monkeypatch): + monkeypatch.setenv("TERMUX_VERSION", "0.119") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + plan = bootstrap_profiles.build_install_plan(requested_profile="auto") + assert plan["profile"] == "android-termux-lean" + assert plan["extras"] == [] + assert "browser automation extras detached" in plan["degraded_capabilities"] + + +def test_build_install_plan_applies_manifest_overrides(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text( + json.dumps( + { + "profile": "android-termux-lean", + "extras_add": ["browser", "search"], + "extras_remove": ["search"], + "notes": ["cloud picked a browser-friendly lane"], + } + ) + ) + plan = bootstrap_profiles.build_install_plan( + requested_profile="android-termux-lean", + manifest_file=str(manifest_path), + ) + assert plan["extras"] == ["browser"] + assert plan["manifest_applied"] is True + assert "cloud picked a browser-friendly lane" in plan["notes"] + assert plan["package_spec"] == "code-puppy[browser]" + + +def test_build_install_plan_rejects_profile_mismatch(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps({"profile": "desktop-browser"})) + with pytest.raises(ValueError, match="does not match"): + bootstrap_profiles.build_install_plan( + requested_profile="android-termux-lean", + manifest_file=str(manifest_path), + ) + + +def test_cli_detect_json(capsys): + exit_code = bootstrap.main(["detect", "--json"]) + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert "python_executable" in payload + assert "is_termux" in payload + + +def test_cli_plan_defaults_to_auto_json(capsys): + exit_code = bootstrap.main(["plan", "--json"]) + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["profile"] in bootstrap_profiles.available_profiles() + assert payload["package_spec"].startswith("code-puppy") + + +def test_build_install_plan_rejects_unknown_profile(): + with pytest.raises(ValueError, match="unknown install profile"): + bootstrap_profiles.build_install_plan(requested_profile="not-a-real-profile") + + +def test_build_install_plan_rejects_multiple_manifest_sources(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text("{}") + with pytest.raises(ValueError, match="choose manifest_json or manifest_file"): + bootstrap_profiles.build_install_plan( + requested_profile="core", + manifest_json="{}", + manifest_file=str(manifest_path), + ) + + +def test_build_install_plan_falls_back_to_pip_without_uv(monkeypatch): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: { + "has_uv": False, + "has_uvx": False, + "has_proot": True, + "has_ripgrep": True, + "has_rust": True, + "has_clang": True, + "is_android": False, + "is_termux": False, + "is_linux": True, + "is_macos": False, + "is_windows": False, + }, + ) + plan = bootstrap_profiles.build_install_plan(requested_profile="core") + assert plan["install_command"] == "python -m pip install code-puppy" + assert plan["reattach_command"] == "python -m pip install --upgrade code-puppy" + + +def test_build_install_plan_reports_missing_android_system_packages(monkeypatch): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: { + "has_uv": True, + "has_uvx": True, + "has_proot": False, + "has_ripgrep": False, + "has_rust": False, + "has_clang": False, + "is_android": True, + "is_termux": True, + "is_linux": True, + "is_macos": False, + "is_windows": False, + }, + ) + plan = bootstrap_profiles.build_install_plan(requested_profile="auto") + assert plan["profile"] == "android-termux-lean" + assert plan["missing_system_packages"] == ["rust", "clang", "ripgrep", "proot"] + + +def test_cli_plan_human_output(capsys): + exit_code = bootstrap.main(["plan"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "Profile:" in output + assert "Install:" in output + assert "Run:" in output + + +def test_cli_plan_rejects_bad_manifest_json(capsys): + with pytest.raises(SystemExit) as exc_info: + bootstrap.main(["plan", "--manifest-json", "[]"]) + assert exc_info.value.code == 2 + assert "bootstrap manifest must be a JSON object" in capsys.readouterr().err diff --git a/tests/test_bootstrap_wizard.py b/tests/test_bootstrap_wizard.py new file mode 100644 index 000000000..1e8894784 --- /dev/null +++ b/tests/test_bootstrap_wizard.py @@ -0,0 +1,156 @@ +from __future__ import annotations + + +from code_puppy import bootstrap, bootstrap_profiles, bootstrap_wizard + + +def _fake_env(**overrides): + base = { + "python_executable": "/usr/bin/python3", + "python_version": "3.13.13", + "platform_system": "Linux", + "platform_release": "android", + "platform_machine": "aarch64", + "has_uv": True, + "has_uvx": True, + "has_proot": True, + "has_ripgrep": True, + "has_rust": True, + "has_clang": True, + "has_git": True, + "is_android": False, + "is_termux": False, + "is_linux": True, + "is_macos": False, + "is_windows": False, + } + base.update(overrides) + return base + + +def _patch_env(monkeypatch, **overrides): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: _fake_env(**overrides), + ) + + +def test_bare_termux_device_builds_three_steps(monkeypatch): + """A fresh phone with nothing installed gets packaged uv -> packages -> code-puppy.""" + + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_uv=False, + has_proot=False, + has_ripgrep=False, + has_rust=False, + has_clang=False, + ) + steps, plan = bootstrap_wizard.build_steps(profile="auto") + keys = [s.key for s in steps] + assert keys == ["uv", "system_packages", "code_puppy"] + + by_key = {s.key: s for s in steps} + # Termux should use its packaged uv instead of building uv from PyPI. + assert by_key["uv"].command == "pkg install -y uv" + assert ( + by_key["system_packages"].command == "pkg install -y rust clang ripgrep proot" + ) + assert plan["profile"] == "android-termux-lean" + + +def test_loaded_device_collapses_to_single_step(monkeypatch): + """When uv + native packages are present, only the install step remains.""" + + _patch_env(monkeypatch, is_android=True, is_termux=True) + steps, _ = bootstrap_wizard.build_steps(profile="auto") + assert [s.key for s in steps] == ["code_puppy"] + + +def test_desktop_missing_uv_uses_curl_installer(monkeypatch): + _patch_env(monkeypatch, has_uv=False) + steps, _ = bootstrap_wizard.build_steps(profile="core") + uv_step = next(s for s in steps if s.key == "uv") + assert "astral.sh/uv/install.sh" in uv_step.command + + +def test_dry_run_executes_nothing(monkeypatch, capsys): + _patch_env(monkeypatch, is_android=True, is_termux=True) + + def _boom(_command): # pragma: no cover - must never be called + raise AssertionError("dry-run must not execute commands") + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _boom) + code = bootstrap_wizard.run_wizard(profile="auto", dry_run=True) + out = capsys.readouterr().out + assert code == 0 + assert "DRY-RUN" in out + assert "dry-run, not executed" in out + + +def test_yes_runs_steps_and_verifies(monkeypatch, capsys): + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_ripgrep=False, + has_proot=False, + has_rust=False, + has_clang=False, + ) + + calls = [] + + def _fake_run(command): + calls.append(command) + return 0, "ok" + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _fake_run) + # Native packages still absent (so the step runs); code-puppy present after install. + monkeypatch.setattr(bootstrap_wizard, "_has", lambda b: b == "code-puppy") + + code = bootstrap_wizard.run_wizard(profile="auto", assume_yes=True) + out = capsys.readouterr().out + assert code == 0 + # system packages + code-puppy install + the verify --help call + assert any("pkg install -y rust clang ripgrep proot" in c for c in calls) + assert any("uv tool install" in c for c in calls) + assert "Verification: OK" in out + + +def test_required_failure_stops_and_returns_one(monkeypatch, capsys): + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_ripgrep=False, + has_proot=False, + has_rust=False, + has_clang=False, + ) + + def _fail(_command): + return 1, "pkg: network unreachable" + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _fail) + code = bootstrap_wizard.run_wizard(profile="auto", assume_yes=True) + out = capsys.readouterr().out + assert code == 1 + assert "FAILED" in out + assert "stopping" in out.lower() + + +def test_verify_fails_when_binary_absent(monkeypatch): + _patch_env(monkeypatch) + monkeypatch.setattr(bootstrap_wizard, "_has", lambda _b: False) + outcome = bootstrap_wizard._verify({}) + assert outcome.status == "failed" + + +def test_cli_wizard_dry_run_via_main(capsys): + exit_code = bootstrap.main(["wizard", "--dry-run"]) + assert exit_code == 0 + assert "install wizard" in capsys.readouterr().out.lower() diff --git a/tests/test_browser_optional.py b/tests/test_browser_optional.py new file mode 100644 index 000000000..e759d4d8d --- /dev/null +++ b/tests/test_browser_optional.py @@ -0,0 +1,45 @@ +"""Browser automation must be an optional capability. + +Playwright has no wheels for some platforms (e.g. Android/Termux), so it is an +optional ``[browser]`` extra. Importing ``code_puppy.tools`` must not crash when +Playwright is unavailable; browser tools should only fail when actually invoked. +""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap + + +def test_tools_import_without_playwright(): + script = textwrap.dedent( + """ + import sys + + # Simulate a platform where Playwright is not installed. + sys.modules["playwright"] = None + sys.modules["playwright.async_api"] = None + + # Must not raise even though Playwright is unavailable. + import code_puppy.tools # noqa: F401 + from code_puppy.tools.browser import browser_manager # noqa: F401 + + # The stub keeps the module importable but inert. + from playwright.async_api import async_playwright + + try: + async_playwright() + except RuntimeError: + print("OK") + else: + raise AssertionError("expected RuntimeError from stubbed async_playwright") + """ + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip().splitlines()[-1] == "OK" diff --git a/uv.lock b/uv.lock index 4776a4596..37f441922 100644 --- a/uv.lock +++ b/uv.lock @@ -310,17 +310,14 @@ dependencies = [ { name = "json-repair" }, { name = "mcp" }, { name = "openai" }, - { name = "pillow" }, - { name = "playwright" }, { name = "prompt-toolkit" }, { name = "pydantic" }, - { name = "pydantic-ai-slim", extra = ["anthropic", "mcp", "openai"] }, + { name = "pydantic-ai-slim", extra = ["anthropic", "mcp"] }, { name = "pyfiglet" }, { name = "python-dotenv" }, { name = "rapidfuzz" }, { name = "requests" }, { name = "rich" }, - { name = "ripgrep" }, { name = "termflow-md" }, { name = "typer" }, ] @@ -329,9 +326,18 @@ dependencies = [ bedrock = [ { name = "boto3" }, ] +browser = [ + { name = "playwright" }, +] durable = [ { name = "dbos" }, ] +images = [ + { name = "pillow" }, +] +search = [ + { name = "ripgrep" }, +] [package.dev-dependencies] dev = [ @@ -353,21 +359,21 @@ requires-dist = [ { name = "json-repair", specifier = ">=0.46.2" }, { name = "mcp", specifier = ">=1.9.4" }, { name = "openai", specifier = ">=1.99.1" }, - { name = "pillow", specifier = ">=10.0.0" }, - { name = "playwright", specifier = ">=1.40.0" }, + { name = "pillow", marker = "extra == 'images'", specifier = ">=10.0.0" }, + { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.40.0" }, { name = "prompt-toolkit", specifier = ">=3.0.52" }, { name = "pydantic", specifier = ">=2.4.0" }, - { name = "pydantic-ai-slim", extras = ["openai", "anthropic", "mcp"], specifier = "==1.56.0" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "mcp"], specifier = "==1.56.0" }, { name = "pyfiglet", specifier = ">=0.8.post1" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "rapidfuzz", specifier = ">=3.13.0" }, { name = "requests", specifier = ">=2.28.0" }, { name = "rich", specifier = ">=13.4.2" }, - { name = "ripgrep", specifier = "==14.1.0" }, + { name = "ripgrep", marker = "extra == 'search'", specifier = "==14.1.0" }, { name = "termflow-md", specifier = ">=0.1.11" }, { name = "typer", specifier = ">=0.12.0" }, ] -provides-extras = ["bedrock", "durable"] +provides-extras = ["bedrock", "browser", "durable", "images", "search"] [package.metadata.requires-dev] dev = [ @@ -1325,10 +1331,6 @@ anthropic = [ mcp = [ { name = "mcp" }, ] -openai = [ - { name = "openai" }, - { name = "tiktoken" }, -] [[package]] name = "pydantic-core" @@ -1745,110 +1747,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[[package]] -name = "regex" -version = "2026.5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, -] - [[package]] name = "requests" version = "2.34.2" @@ -2168,60 +2066,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/49/0fa8324879351c59ceb169f443c578fd6f8272ee3e71268f0b9140589614/termflow_md-0.1.11-py3-none-any.whl", hash = "sha256:4fd6f375b4a06aa06987dfc80d2910fed2b27e9576c50d49c7f3903c67195b2a", size = 58710, upload-time = "2026-04-18T17:05:23.738Z" }, ] -[[package]] -name = "tiktoken" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, - { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, - { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, - { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, - { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, - { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, - { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, - { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, - { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, - { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, - { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, - { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, - { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, - { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, - { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, - { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, -] - [[package]] name = "tomli" version = "2.4.1"