diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md index 331f268..f4fb2db 100644 --- a/.claude/rules/code-style.md +++ b/.claude/rules/code-style.md @@ -1,7 +1,11 @@ # Code Style Rules +- Python 3.14+. Use `uv sync --extra dev` to install. +- mypy strict mode. All code must pass `mypy src/` with `strict = true`. - No `Any` type annotations for dataclass fields. Use `TYPE_CHECKING` imports to break circular dependencies. +- `from __future__ import annotations` at top of every Python file. - Pydantic v2 with `frozen=True` for all data models. - Async-first. All RIMAPI calls are async. Game loop is async. +- No scipy/numpy. Use stdlib only (random, math, statistics). See ADR-003. - Tests use pytest-asyncio with `asyncio_mode = "auto"`. -- Run `pytest` and `ruff check src/ tests/ scripts/` before committing. All must pass. +- Run `pytest`, `ruff check src/ tests/ scripts/`, and `mypy src/` before committing. All must pass. diff --git a/.claude/settings.json b/.claude/settings.json index 7ea49fa..1680011 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,7 +21,11 @@ "Bash(OPENAI_API_KEY=*)", "Bash(PYTHONIOENCODING=*)", "Bash(bun run:*)", - "Bash(bun install:*)" + "Bash(bun install:*)", + "Bash(python -m mypy src/rle/)", + "Bash(python -m uv run pytest --tb=short -q)", + "Bash(python -m uv run ruff check src/ tests/ scripts/)", + "Bash(python -m uv run python scripts/run_benchmark.py --dry-run --ticks 3)" ], "additionalDirectories": [ "c:\\Users\\redmo\\Projects\\rimapi-dashboard\\src\\components\\widgets\\rle", diff --git a/.claude/skills/benchmark/SKILL.md b/.claude/skills/benchmark/SKILL.md index e77314f..df567dc 100644 --- a/.claude/skills/benchmark/SKILL.md +++ b/.claude/skills/benchmark/SKILL.md @@ -2,19 +2,35 @@ name: benchmark description: Run the full RLE benchmark suite (6 scenarios) allowed-tools: Bash, Read -argument-hint: [--ticks N] [--dry-run] [--sequential] +argument-hint: "--ticks N --smoke-test --docker --runs N --sequential" --- -Run the RLE benchmark. Read `.env` for OPENROUTER_API_KEY. Default: OpenRouter with Nemotron 120B, 10 ticks, parallel, no-think. +Run the RLE benchmark using provider/model from `.env`. Read `.env` first to determine the configuration. ```bash -OPENAI_API_KEY= python scripts/run_benchmark.py \ +# .env has PROVIDER, MODEL, PROVIDER_BASE_URL, OPENAI_API_KEY +source .env 2>/dev/null +python scripts/run_benchmark.py \ + --no-think --visualize \ + --output results/benchmark-latest/ \ + $ARGUMENTS +``` + +If `.env` isn't configured or user specifies a provider, override with CLI flags: +```bash +python scripts/run_benchmark.py \ --provider openai \ - --model nvidia/nemotron-3-super-120b-a12b \ - --base-url https://openrouter.ai/api/v1 \ + --model \ + --base-url \ --no-think --visualize \ --output results/benchmark-latest/ \ $ARGUMENTS ``` +Key flags: +- `--smoke-test` — Mock RIMAPI (no game needed) +- `--docker` — Use headless RimWorld container +- `--runs N` — Paired runs for statistical validity (N>=4 for leaderboard) +- `--no-baseline` — Skip baseline comparison + When complete, show the leaderboard and ask if results should be posted to a GitHub issue. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..211dc63 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +__pycache__ +*.pyc +.env +results/ +*.egg-info +.mypy_cache +.ruff_cache +.claude/ diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..2232ddc --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,83 @@ +name: Benchmark + +on: + workflow_dispatch: + inputs: + model: + description: "Model name (e.g., nvidia/nemotron-3-super-120b-a12b:free)" + required: true + type: string + provider: + description: "Provider" + required: true + default: "openai" + type: choice + options: + - openai + - anthropic + base_url: + description: "Provider base URL (e.g., https://openrouter.ai/api/v1)" + required: false + type: string + runs: + description: "Number of paired runs (N>=4 for statistical validity)" + required: false + default: "4" + type: string + schedule: + - cron: "0 6 * * 1" # Weekly Monday 6am UTC + +jobs: + benchmark: + runs-on: ubuntu-latest + # Full Docker benchmarks require a self-hosted runner with: + # - Docker installed + pre-built rle-headless image + # - LLM provider API key in secrets + # This workflow runs smoke-test on GitHub-hosted runners. + # Docker steps are commented out as a template. + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@v4 + - run: uv pip install -e ".[dev]" --system + + - name: Smoke test + run: python scripts/run_benchmark.py --dry-run --ticks 5 + + # --- Docker benchmark (uncomment when self-hosted runner is configured) --- + # + # - name: Start HeadlessRim container + # run: docker compose -f docker/docker-compose.yml up -d + # + # - name: Wait for RIMAPI + # run: | + # for i in $(seq 1 24); do + # curl -sf http://localhost:8765/api/v1/game/state && exit 0 + # sleep 5 + # done + # exit 1 + # + # - name: Run benchmark + # env: + # OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # run: | + # python scripts/run_benchmark.py \ + # --docker \ + # --provider ${{ inputs.provider || 'openai' }} \ + # --model "${{ inputs.model || 'nvidia/nemotron-3-super-120b-a12b:free' }}" \ + # ${{ inputs.base_url && format('--base-url {0}', inputs.base_url) || '' }} \ + # --runs ${{ inputs.runs || '4' }} \ + # --no-think \ + # --output results/ci/ + # + # - name: Stop container + # if: always() + # run: docker compose -f docker/docker-compose.yml down + # + # - name: Upload results + # uses: actions/upload-artifact@v4 + # with: + # name: benchmark-results + # path: results/ci/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..082b3c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [master, "feature/**"] + pull_request: + branches: [master, "feature/**"] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@v4 + - run: uv pip install -e ".[dev]" --system + - name: Lint + run: ruff check src/ tests/ scripts/ + - name: Type check + run: mypy src/ + + test: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@v4 + - run: uv pip install -e ".[dev]" --system + - name: Run tests + run: pytest --tb=short -q + + smoke-test: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@v4 + - run: uv pip install -e ".[dev]" --system + - name: Smoke test benchmark + run: python scripts/run_benchmark.py --dry-run --ticks 5 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/CLAUDE.md b/CLAUDE.md index 070ccdb..e34eb00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,11 +53,13 @@ curl http://localhost:1234/v1/models ## Commands -- Install: `pip install -e ".[dev]"` +- Install: `uv sync --extra dev` - Test: `pytest` - Lint: `ruff check src/ tests/ scripts/` - Type check: `mypy src/` - List scenarios: `python scripts/run_scenario.py --list` +- Smoke test: `python scripts/run_benchmark.py --smoke-test --ticks 5` +- Compare runs: `python scripts/compare_benchmarks.py results/run1 results/run2` ### Configure `.env` @@ -127,12 +129,34 @@ cd ../rimapi-dashboard && bun run start # Open http://localhost:3000 ``` -### Mock benchmark (no game needed) +### Smoke test (no game needed) ```bash -python scripts/run_benchmark.py --dry-run --ticks 10 +python scripts/run_benchmark.py --smoke-test --ticks 10 ``` +### Docker benchmark (no display needed) + +```bash +# Build the headless image (see docker/README.md for prerequisites) +docker compose -f docker/docker-compose.yml up -d + +# Run benchmark against containerized game +python scripts/run_benchmark.py --docker --provider openai \ + --model nvidia/nemotron-3-super-120b-a12b:free \ + --base-url https://openrouter.ai/api/v1 \ + --no-think --runs 4 --output results/docker/ +``` + +**Benchmark flags:** +- `--smoke-test` — Mock RIMAPI (replaces deprecated `--dry-run`) +- `--docker` — Use Docker container for headless RimWorld +- `--runs N` — Paired runs per scenario (N≥4 for statistical validity) +- `--no-baseline` — Skip baseline (no-agent) comparison runs +- `--ablation` — (WIP) Run with each agent removed to measure contribution +- `--wandb` — Log to Weights & Biases +- `--push-hf` — Push results to HuggingFace Hub (requires `--runs 4+`) + ## Architecture ``` @@ -211,7 +235,7 @@ Agents communicate through Felix SDK's CentralPost, not through the orchestrator - **Before deliberation**: `process_all_messages()` routes previous tick's messages to agent spoke inbound queues. Agents read via `_get_spoke_context()`. - **MapAnalyst first**: Deliberates, sends TASK_COMPLETE with spatial analysis. Messages routed immediately so role agents see it. - **After deliberation**: Each role agent sends `TASK_COMPLETE` with role, summary, confidence, action types. -- **After scoring**: Hub broadcasts `STATUS_UPDATE` with composite score + all 8 metrics. +- **After scoring**: Hub broadcasts `STATUS_UPDATE` with composite score + all 10 metrics. - **On phase change**: Hub broadcasts `PHASE_ANNOUNCE` when macro_time crosses 0.4 (exploration→analysis) or 0.7 (analysis→synthesis). ## SSE Events @@ -236,7 +260,7 @@ Macro helix: `t = min(1.0, game_day / expected_duration_days)` drives agent beha - **Analysis** (0.4 <= t < 0.7): Medium temp, evaluate trade-offs - **Synthesis** (t >= 0.7): Low temperature, decisive actions -## Scoring (8 metrics, weighted composite) +## Scoring (10 metrics, weighted composite) | Metric | Default Weight | Source | |--------|---------------|--------| @@ -248,6 +272,10 @@ Macro helix: `t = min(1.0, game_day / expected_duration_days)` drives agent beha | research | 0.10 | % research tree completed | | self_sufficiency | 0.10 | power + food + population stability | | efficiency | 0.05 | action execution rate | +| coordination | 0.00* | conflicts resolved / total conflicts | +| communication_efficiency | 0.00* | messages acted on / total messages | + +*Process metrics have 0.0 weight until game loop wires MetricContext counters. Target: coordination=0.12, communication_efficiency=0.08. Scenarios can override weights. TimeSeriesRecorder exports per-tick CSV. @@ -279,14 +307,24 @@ Use `--no-think` for thinking models (Qwen3.5, Nemotron) — injects `` ## Conventions +- Python 3.14+, `uv` for package management, `hatchling` build backend - Async-first (httpx AsyncClient, async game loop) - Parallel-first: MapAnalyst runs first (sequential), then 6 role agents deliberate concurrently via `asyncio.to_thread` + `asyncio.gather` (`--sequential` to disable) - Pydantic v2 models with frozen=True for game state and results +- mypy strict mode — all code must pass `mypy src/` with `strict = true` +- No scipy/numpy — stdlib only for statistics (random, math). See ADR-003 for rationale - Felix Agent SDK for providers, agents, helix geometry, CentralPost communication - JSON repair + parse retry for LLM output resilience (strips think tags, trailing commas, extracts first JSON object) - Real RIMAPI data via state adapters + deterministic terrain analysis - Tests use pytest-asyncio with auto mode +## CI/CD + +GitHub Actions workflows in `.github/workflows/`: + +- **ci.yml** — On every push/PR: ruff lint, mypy strict, pytest, smoke-test +- **benchmark.yml** — Manual dispatch + weekly schedule: Docker benchmark template (requires self-hosted runner with game files) + ## Package Structure ``` @@ -312,13 +350,33 @@ src/rle/ │ ├── state_manager.py # GameStateManager (SSE drain, macro time, history) │ ├── action_executor.py # Routes actions to RIMAPI write endpoints │ └── action_resolver.py # 4-rule conflict resolution -├── scoring/ # 8 metrics, composite scorer, CSV recorder +├── scoring/ # 10 metrics, composite scorer, bootstrap CIs, CSV recorder +│ ├── metrics.py # 10 individual metric functions (8 colony + 2 process) +│ ├── composite.py # CompositeScorer (weighted aggregation) +│ ├── bootstrap.py # BootstrapCI, bootstrap_ci(), bootstrap_paired_delta() +│ ├── delta.py # PairedResult (agent vs baseline stats, Welch's t-test) +│ └── recorder.py # TimeSeriesRecorder (per-tick CSV export) +├── tracking/ # Benchmark history, cost tracking, observability +│ ├── cost_tracker.py # CostTracker + OpenRouter pricing API +│ ├── event_log.py # Structured JSONL event log (deliberations, actions, errors) +│ ├── leaderboard.py # Model×scenario matrix, Pareto frontier +│ ├── history.py # JSONL run history + per-model baselines +│ ├── metadata.py # Git commit, versions, reproducibility metadata +│ ├── wandb_logger.py # Weights & Biases integration (optional) +│ └── hf_logger.py # HuggingFace Hub export (optional) +├── docker.py # DockerGameServer lifecycle + wait_for_rimapi() └── scenarios/ # YAML schema, loader, evaluator, 6 definitions scripts/ ├── run_scenario.py # Single scenario CLI (auto-loads save, unforbids items) -├── run_benchmark.py # Full benchmark suite CLI +├── run_benchmark.py # Full benchmark suite CLI (--docker, --smoke-test, --runs) +├── compare_benchmarks.py # Paired statistical comparison of benchmark runs ├── visualize_results.py # Matplotlib CSV plotter └── serve_dashboard.py # CORS-enabled file server for dashboard +docker/ +├── Dockerfile # HeadlessRim + Xvfb (debian:bookworm-slim) +├── docker-compose.yml # Volume mounts for game files, mods, saves +├── entrypoint.sh # Xvfb → RimWorld → RIMAPI healthcheck +└── README.md # Docker setup prerequisites and troubleshooting ## Related Repos diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b6964e..323b064 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,18 +5,22 @@ ```bash git clone https://github.com/AppSprout-dev/RLE.git cd RLE -pip install -e ".[dev]" -pytest # should pass 262+ tests +uv sync --extra dev +pytest # should pass 326+ tests ``` +Requires Python 3.14+ and [uv](https://docs.astral.sh/uv/). + ## Development workflow 1. Create a branch from `master` 2. Make changes -3. Run `pytest` and `ruff check src/ tests/ scripts/` +3. Run `pytest`, `ruff check src/ tests/ scripts/`, and `mypy src/` 4. Commit with a descriptive message 5. Open a PR against `master` +CI runs lint + type check + tests + smoke test on every push/PR. + ## Running locally ### Prerequisites @@ -27,6 +31,7 @@ pytest # should pass 262+ tests | RimWorld + RIMAPI mod | Game state + actions | 8765 | | Dashboard (optional) | Live visualization | 3000 | | Tick data server (optional) | Dashboard data feed | 9000 | +| Docker (optional) | Headless benchmarks | 8765 | ### Recommended local model @@ -37,12 +42,12 @@ LM Studio settings: Flash Attention ON, Context 10000, GPU Offload max, Keep in ### Quick test (no RimWorld needed) ```bash -# Mock benchmark — tests the full pipeline with fake game state -python scripts/run_benchmark.py --dry-run --ticks 3 +# Smoke test — tests the full pipeline with fake game state +python scripts/run_benchmark.py --smoke-test --ticks 3 -# Mock benchmark with real LLM (needs LM Studio running) +# Smoke test with real LLM (needs LM Studio running) OPENAI_API_KEY=lm-studio python scripts/run_benchmark.py \ - --dry-run --provider openai \ + --smoke-test --provider openai \ --model unsloth/nvidia-nemotron-3-nano-4b \ --base-url http://localhost:1234/v1 \ --no-think --ticks 3 @@ -59,6 +64,16 @@ OPENAI_API_KEY=lm-studio python scripts/run_scenario.py crashlanded_survival \ --no-think --visualize --ticks 10 ``` +### Docker benchmark (headless, no display) + +```bash +# Build image (see docker/README.md for prerequisites) +docker compose -f docker/docker-compose.yml up -d + +# Run benchmark against container +python scripts/run_benchmark.py --docker --runs 4 --output results/docker/ +``` + ### OpenRouter (cloud, no local GPU needed) ```bash @@ -71,9 +86,12 @@ OPENAI_API_KEY= python scripts/run_benchmark.py \ ## Code conventions +- **Python 3.14+** — `uv` for package management, `hatchling` build backend +- **mypy strict** — all code must pass `mypy src/` with `strict = true` - **Async-first** — httpx AsyncClient, async game loop - **Pydantic v2** — frozen models for all data structures - **No `Any` types** in metric contexts — use `TYPE_CHECKING` imports to break circular deps +- **No scipy/numpy** — stdlib only for statistics (see ADR-003) - **Parallel by default** — agents deliberate concurrently via `asyncio.to_thread` - **JSON repair** — LLM output goes through `json_repair.py` before parsing - **CentralPost for inter-agent context** — not orchestrator-passed lists @@ -88,17 +106,10 @@ OPENAI_API_KEY= python scripts/run_benchmark.py \ 5. Register in `src/rle/agents/__init__.py` — add to `_ROLE_AGENTS` and `AGENT_DISPLAY` 6. Add tests in `tests/unit/test_role_agents.py` -## Adding a new action type - -1. Add to `ActionType` enum in `src/rle/agents/actions.py` -2. Add a handler in `src/rle/orchestration/action_executor.py` -3. Add the RIMAPI client method in `src/rle/rimapi/client.py` -4. Add to the relevant agent's `ALLOWED_ACTIONS` - ## Adding a new scenario 1. Create `src/rle/scenarios/definitions/NN_your_scenario.yaml` -2. Follow the schema: name, description, difficulty, expected_duration_days, initial_population, victory_conditions, failure_conditions, max_ticks, scoring_weights +2. Follow the schema: name, description, difficulty, expected_duration_days, initial_population, victory_conditions, failure_conditions, max_ticks, scoring_weights (include all 10 metrics) 3. The loader auto-discovers YAML files — no registration needed ## Project structure @@ -106,11 +117,15 @@ OPENAI_API_KEY= python scripts/run_benchmark.py \ ``` src/rle/ ├── config.py # RLEConfig (env vars, provider, helix preset) +├── docker.py # Docker container lifecycle + RIMAPI health checks ├── rimapi/ # RIMAPI client + SSE + schemas -├── agents/ # 6 role agents + base class + actions + JSON repair +├── agents/ # 7 agents (MapAnalyst + 6 role) + base class + JSON repair ├── orchestration/ # Game loop, state manager, executor, resolver -├── scoring/ # 8 metrics, composite scorer, CSV recorder +├── scoring/ # 10 metrics, composite scorer, bootstrap CIs, CSV recorder +├── tracking/ # Cost tracking, event log, leaderboard, W&B/HF loggers └── scenarios/ # YAML schema, loader, evaluator, 6 definitions +docker/ # HeadlessRim Dockerfile, compose, entrypoint +.github/workflows/ # CI (lint+test+smoke) and benchmark (Docker) workflows ``` ## Key dependencies @@ -118,6 +133,7 @@ src/rle/ - [felix-agent-sdk](https://github.com/AppSprout-dev/felix-agent-sdk) >= 0.2.1 - [RIMAPI](https://github.com/IlyaChichkov/RIMAPI) C# mod - httpx, pydantic >= 2.0, pyyaml +- Optional: wandb, huggingface-hub (`uv sync --extra tracking`) ## Questions? diff --git a/README.md b/README.md index b262b6f..58ed6fc 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ You need four things set up: 1. **RimWorld** (Steam) with **Harmony** and **[RIMAPI](https://github.com/IlyaChichkov/RIMAPI)** mods subscribed and **enabled** in the Mods menu. Load order: Harmony → Core → (DLCs) → RIMAPI. 2. **LLM provider** — [LM Studio](https://lmstudio.ai/) (local, free) or [OpenRouter](https://openrouter.ai/) (cloud) -3. **Python 3.10+** with pip +3. **Python 3.14+** with [uv](https://docs.astral.sh/uv/) 4. **Save file** — `rle_crashlanded_v1` in RimWorld's save folder (the scenario auto-loads it) > **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. See [CLAUDE.md](CLAUDE.md) for instructions on building and deploying our fork DLL. @@ -63,7 +63,7 @@ curl http://localhost:1234/v1/models # LM Studio (if using local) ```bash git clone https://github.com/AppSprout-dev/RLE.git cd RLE -pip install -e ".[dev]" +uv sync --extra dev ``` ### Configure `.env` @@ -182,7 +182,7 @@ All colonists alive, buildings on solid ground, no water placement. ## Scoring -8 metrics, weighted composite (scenarios can override weights): +10 metrics, weighted composite (scenarios can override weights): | Metric | Default Weight | What it measures | |--------|---------------|------------------| @@ -194,6 +194,10 @@ All colonists alive, buildings on solid ground, no water placement. | research | 0.10 | % research tree completed | | self_sufficiency | 0.10 | power + food + population stability | | efficiency | 0.05 | action execution rate | +| coordination | 0.00* | conflicts resolved / total conflicts | +| communication_efficiency | 0.00* | messages acted on / total messages | + +*Process metrics weighted 0.0 until game loop wires MetricContext counters. ## Development diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..d6fa336 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,8 @@ +# Path to Linux RimWorld game files (via SteamCMD: app_update 294100 -platform linux) +RIMWORLD_PATH=/path/to/rimworld + +# Path to RIMAPI mod directory (containing About/ and 1.6/Assemblies/RIMAPI.dll) +RIMAPI_MOD_PATH=/path/to/rimapi-mod + +# RIMAPI port (default 8765) +RIMAPI_PORT=8765 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..bbb416f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,43 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + xvfb libx11-6 libgl1-mesa-glx libgl1-mesa-dri mesa-utils \ + dbus libpulse0 alsa-utils openbox \ + libxcursor1 libxrandr2 libxinerama1 libxi6 libxss1 libxtst6 \ + libfontconfig1 libxrender1 libsdl2-2.0-0 \ + curl netcat-openbsd procps ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN printf 'pcm.!default {\n type null\n}\nctl.!default {\n type null\n}\n' > /etc/asound.conf + +ENV DISPLAY=:99 \ + SDL_AUDIODRIVER=dummy \ + AUDIODEV=null \ + PULSE_SERVER= + +RUN useradd -m -s /bin/bash rimworld \ + && mkdir -p /opt/mods/HeadlessRimPatch /opt/mods/RIMAPI /opt/saves /opt/mods-merged \ + && chown -R rimworld:rimworld /opt/mods /opt/saves /opt/mods-merged + +# Download HeadlessRimPatch v1.0.0 prebuilt DLL from GitHub Releases +ARG HEADLESS_PATCH_VERSION=1.0.0 +RUN curl -fsSL \ + "https://github.com/IlyaChichkov/HeadlessRimPatch/releases/download/v${HEADLESS_PATCH_VERSION}/HeadlessRimPatch-${HEADLESS_PATCH_VERSION}.zip" \ + -o /tmp/patch.zip \ + && apt-get update && apt-get install -y --no-install-recommends unzip \ + && unzip /tmp/patch.zip -d /opt/mods/HeadlessRimPatch \ + && rm /tmp/patch.zip \ + && apt-get purge -y unzip && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER rimworld + +EXPOSE 8765 + +HEALTHCHECK --interval=10s --timeout=5s --retries=12 --start-period=60s \ + CMD curl -sf http://localhost:8765/api/v1/game/state || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..38dde16 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,58 @@ +# HeadlessRim Docker — Automated RLE Benchmarks + +Runs RimWorld headlessly in Docker with RIMAPI for automated benchmark sessions. + +## Prerequisites + +1. **Linux RimWorld game files** — Download via SteamCMD: + ```bash + steamcmd +login +app_update 294100 -platform linux +quit + ``` + +2. **RIMAPI mod** — Built from [our fork](https://github.com/AppSprout-dev/RIMAPI) (`rle-testing` branch): + ```bash + cd RIMAPI/Source/RIMAPI && dotnet build RimApi.csproj -c Release-1.6 + ``` + +3. **HeadlessRimPatch** — Downloaded automatically in the Dockerfile from [GitHub Releases](https://github.com/IlyaChichkov/HeadlessRimPatch/releases) (v1.0.0). Alternatively, mount a local build. + +4. **Save files** — Place RLE scenario saves in `docker/saves/`. + +## Quick Start + +```bash +cp .env.example .env +# Edit .env with your paths +docker compose up -d +# Wait for healthcheck (~60-120s) +curl http://localhost:8765/api/v1/game/state +``` + +## How It Works + +1. Container starts Xvfb on `:99` (virtual display) +2. RimWorldLinux launches in `-batchmode -nographics` mode +3. HeadlessRimPatch patches out Unity UI/rendering, calls `SetupForQuickTestPlay()` to auto-start a game +4. RIMAPI starts serving on `:8765` once a map loads +5. RLE orchestrator connects and loads the benchmark save via `POST /api/v1/game/load` + +## Running Benchmarks + +```bash +# From the RLE project root: +python scripts/run_benchmark.py --docker --provider openai \ + --model nvidia/nemotron-3-super-120b-a12b:free \ + --base-url https://openrouter.ai/api/v1 \ + --no-think --runs 4 --output results/docker/ +``` + +## Known Issues + +- **RIMAPI IPv6 binding**: RIMAPI binds to `[::1]:8765` inside the container. Docker port forwarding maps `0.0.0.0:8765` → container. If RIMAPI only listens on IPv6 loopback, the port forward won't reach it. May need RIMAPI config change. +- **Startup time**: HeadlessRimPatch generates a throwaway map before we load our save. Feature request filed ([HeadlessRimPatch#5](https://github.com/IlyaChichkov/HeadlessRimPatch/issues/5)) for direct save loading via env var. + +## References + +- [IlyaChichkov/HeadlessRim](https://github.com/IlyaChichkov/HeadlessRim) — Docker setup +- [IlyaChichkov/HeadlessRimPatch](https://github.com/IlyaChichkov/HeadlessRimPatch) — Harmony patches for headless mode +- [HeadlessRim#1](https://github.com/IlyaChichkov/HeadlessRim/issues/1) — RIMAPI compatibility confirmed diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..09a4e27 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +services: + rimworld: + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - "${RIMAPI_PORT:-8765}:8765" + volumes: + - ${RIMWORLD_PATH:?Set RIMWORLD_PATH to Linux RimWorld install}:/opt/game:ro + - ${RIMAPI_MOD_PATH:?Set RIMAPI_MOD_PATH to RIMAPI mod directory}:/opt/mods/RIMAPI:ro + - ./saves:/opt/saves:ro + shm_size: 1gb diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..959adfc --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -e + +# Clean stale X11 locks +rm -f /tmp/.X99-lock /tmp/.X11-unix/X99 + +# Start Xvfb +Xvfb :99 -screen 0 1280x720x24 -ac +extension GLX & +XVFB_PID=$! + +# Wait for X socket +ELAPSED=0 +while [ ! -e /tmp/.X11-unix/X99 ]; do + sleep 0.5 + ELAPSED=$((ELAPSED + 1)) + if [ "$ELAPSED" -ge 60 ]; then + echo "ERROR: Xvfb failed to start within 30s" + exit 1 + fi +done +echo "Xvfb ready on :99" + +# DBus machine-id (prevents Unity startup crash) +dbus-uuidgen --ensure 2>/dev/null || true + +# Pre-seed RIMAPI config: bind to 0.0.0.0 instead of localhost (IPv6 loopback) +# so Docker port forwarding can reach the server from the host. +CONFIG_DIR="$HOME/.config/unity3d/Ludeon Studios/RimWorld by Ludeon Studios/Config" +mkdir -p "$CONFIG_DIR" +if [ ! -f "$CONFIG_DIR/Mod_RedEyeDev.RIMAPI_Settings.xml" ]; then + cat > "$CONFIG_DIR/Mod_RedEyeDev.RIMAPI_Settings.xml" << 'XMLEOF' + + + 0.0.0.0 + 8765 + +XMLEOF +fi + +# Link save files into RimWorld saves directory +SAVES_DIR="$HOME/.config/unity3d/Ludeon Studios/RimWorld by Ludeon Studios/Saves" +mkdir -p "$SAVES_DIR" +if [ -d /opt/saves ]; then + for f in /opt/saves/*; do + [ -e "$f" ] && ln -sf "$f" "$SAVES_DIR/$(basename "$f")" + done +fi + +# Link mods into a writable overlay (game dir is mounted :ro) +MODS_DIR="/opt/mods-merged" +mkdir -p "$MODS_DIR" +# Copy game's built-in mods (read-only source) +if [ -d /opt/game/Mods ]; then + for mod in /opt/game/Mods/*/; do + [ -d "$mod" ] && ln -sf "$mod" "$MODS_DIR/$(basename "$mod")" + done +fi +# Link our additional mods on top +for mod_dir in /opt/mods/*/; do + [ -d "$mod_dir" ] && ln -sf "$mod_dir" "$MODS_DIR/$(basename "$mod_dir")" +done + +# Launch RimWorld +echo "Starting RimWorld headless..." +/opt/game/RimWorldLinux \ + -batchmode \ + -nographics \ + -noshaders \ + -force-opengl \ + -startServer \ + -logFile /tmp/rimworld_log.txt & +GAME_PID=$! + +# Wait for RIMAPI to become responsive +echo "Waiting for RIMAPI on :8765..." +ELAPSED=0 +while ! curl -sf http://localhost:8765/api/v1/game/state > /dev/null 2>&1; do + sleep 5 + ELAPSED=$((ELAPSED + 5)) + if [ "$ELAPSED" -ge 120 ]; then + echo "ERROR: RIMAPI not responsive after 120s" + tail -30 /tmp/rimworld_log.txt 2>/dev/null + exit 1 + fi +done +echo "RIMAPI ready" + +# Keep container alive +tail -f /tmp/rimworld_log.txt diff --git a/docs/adr/003-automated-benchmark-infrastructure.md b/docs/adr/003-automated-benchmark-infrastructure.md new file mode 100644 index 0000000..aa476b6 --- /dev/null +++ b/docs/adr/003-automated-benchmark-infrastructure.md @@ -0,0 +1,101 @@ +# ADR-003: Automated Benchmark Infrastructure + +**Date:** 2026-04-09 +**Status:** Accepted +**Deciders:** @jkbennitt, @calebisgross + +## Decision + +Replace mock benchmarks with Docker-containerized HeadlessRim for real automated game sessions. Expand scoring from 8 to 10 metrics (adding coordination + communication_efficiency). Add bootstrap confidence intervals, real-time cost tracking via OpenRouter API, and structured event logging for full decision trace observability. + +## Context + +Current `--dry-run` benchmarks test JSON parse rate against static mock data — zero signal about colony management quality. The mock state never changes (3 colonists, 8000 wealth, flat metrics every tick). The leaderboard vision (#8) requires 6 scenarios × N models × 4+ runs = hundreds of automated game sessions. This is impossible manually. + +IlyaChichkov confirmed RIMAPI works headlessly (HeadlessRim#1, 2026-03-22) and published HeadlessRimPatch v1.0.0 the same day. The patch strips Unity UI/rendering via Harmony, enabling RimWorld to run in Docker with Xvfb. + +Industry research (FLE/NeurIPS 2025, SWE-bench, ARC-AGI-2, WebArena, Chatbot Arena, M3-Bench) identified patterns we adopt: + +- Docker isolation for reproducibility (SWE-bench, FLE) +- Bootstrap confidence intervals (14/24 major benchmarks skip this per BetterBench 2024) +- Cost-per-task metrics (ARC-AGI-2's key innovation: accuracy without cost is meaningless) +- Process-aware evaluation (M3-Bench: score HOW agents coordinate, not just outcomes) +- RimWorld's stochastic events provide built-in contamination resistance + +## Decision Drivers + +- `--dry-run` produces zero signal — agents always score 0.706 on static data +- No benchmark without a live game, no live game automation without Docker +- Multi-model leaderboard requires 96+ automated game sessions (6 scenarios × 4 models × 4 runs) +- Process metrics (coordination, communication) differentiate RLE from single-agent benchmarks +- Cost tracking enables ARC-AGI-2-style accuracy-vs-cost Pareto frontiers + +## What Changes + +### 1. Docker infrastructure (HeadlessRim + HeadlessRimPatch + RIMAPI) + +Docker container runs headless RimWorld with RIMAPI on :8765. Game files mounted at runtime (not baked in) to avoid distributing copyrighted content. Entrypoint: Xvfb → RimWorldLinux → HeadlessRimPatch auto-starts game → RIMAPI serves → we load benchmark save via REST. + +### 2. Scoring: 8 → 10 metrics with process awareness + +Two new metrics folded into the composite score: +- `coordination`: conflicts resolved / total conflicts (from ActionResolver) +- `communication_efficiency`: messages acted on / total messages sent + +Process metrics get 20% combined weight. No historical data to protect (pre-release). All 6 scenario YAMLs updated. + +### 3. Bootstrap confidence intervals (stdlib-only) + +We evaluated `resample` (scikit-hep, best modern option) but it pulls in scipy (~150MB) + numpy (~50MB). For percentile bootstrap CIs and Welch's t-test at N≥4, stdlib `random.choices()` + `math` is mathematically correct and keeps the install lightweight. The existing hand-rolled Welch's t-test in `delta.py` uses a normal CDF approximation (Abramowitz & Stegun) documented as "very accurate for df > 30, approximate for smaller df." Our N≥4 minimum guarantees sufficient accuracy. + +If we later need BCa intervals or power analysis, we upgrade to scipy — the `BootstrapCI` pydantic model API won't change. + +### 4. Cost tracking via OpenRouter API + +Real-time per-token pricing from `GET https://openrouter.ai/api/v1/models` (public, no auth). Works for all providers (Anthropic, OpenAI, Meta, etc.) since OpenRouter lists every model. Graceful fallback to $0.00 if unreachable. Cached per benchmark run. + +### 5. Structured event log (dual observability) + +Append-only JSONL capturing every event: deliberations (raw LLM output + parsed plan), conflicts, action executions (RIMAPI call + response), scores, errors. This is the offline source of truth. + +W&B Weave provides optional rich LLM trace visualization on top — same events, interactive UI. Graceful degradation if wandb not installed. + +### 6. N≥4 enforcement for leaderboard submissions + +`--push-hf` requires `--runs 4` or higher. Warning printed for N < 4. Bootstrap CIs require multiple runs to be meaningful. + +## Alternatives Rejected + +1. **Keep mock benchmarks** — zero signal about colony management quality. +2. **Build simulation mode with advancing fake state** — we'd be building a fake RimWorld engine. The real game has 156+ interacting systems. +3. **Manual live runs only** — doesn't scale beyond 2 models, no CI integration, no reproducibility. +4. **scipy/numpy for statistics** — `resample` package requires scipy (~150MB) + numpy (~50MB). Percentile bootstrap with stdlib is mathematically correct for our N≥4 use case and keeps install under 5 seconds. + +## Consequences + +**Positive:** +- Real benchmarks with real game state that changes per tick +- Multi-model leaderboard becomes automatable (Docker + CI) +- Process metrics differentiate RLE from all existing benchmarks +- Cost-normalized rankings prevent "just throw more compute at it" gaming +- Full decision traces enable post-hoc analysis of model behavior +- Bootstrap CIs give credible statistical comparisons + +**Negative:** +- Linux RimWorld game files required for Docker (via SteamCMD) +- Historical mock benchmark data invalidated (acceptable: pre-release) +- Docker image size (~2GB with game files mounted) +- OpenRouter API dependency for cost tracking (graceful fallback) + +**Risks:** +- RIMAPI IPv6 binding in Docker — may need config for port forwarding. Test early. +- HeadlessRimPatch `SetupForQuickTestPlay()` creates a throwaway map before we load our save — wasted startup time. Feature request filed (HeadlessRimPatch#5). +- Small models may struggle with 10 metrics in scoring — mitigated by per-scenario weight overrides. + +## Related + +- Issue #13: HeadlessRim Docker — real automated benchmarks + leaderboard infrastructure +- Issue #8: RLE v1.0 multi-model colony management leaderboard +- Issue #6: Agents must beat unmanaged baseline +- HeadlessRim#1: RIMAPI compatibility confirmed +- HeadlessRimPatch#5: Feature request for direct save loading diff --git a/pyproject.toml b/pyproject.toml index f82eb06..e292df8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=68.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "rimworld-learning-environment" @@ -8,7 +8,7 @@ version = "0.1.0" description = "Multi-agent benchmark where Felix Agent SDK agents play RimWorld" readme = "README.md" license = "MIT" -requires-python = ">=3.10" +requires-python = ">=3.14" dependencies = [ "felix-agent-sdk>=0.2.1", "httpx>=0.24", @@ -28,6 +28,7 @@ dev = [ "pytest-cov>=4.0", "ruff>=0.1.0", "mypy>=1.0", + "types-PyYAML>=6.0", ] viz = [ "matplotlib>=3.5", @@ -37,8 +38,8 @@ tracking = [ "huggingface-hub>=0.20", ] -[tool.setuptools.packages.find] -where = ["src"] +[tool.hatch.build.targets.wheel] +packages = ["src/rle"] [tool.pytest.ini_options] asyncio_mode = "auto" @@ -46,11 +47,15 @@ testpaths = ["tests"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py314" [tool.ruff.lint] select = ["E", "F", "I", "W"] [tool.mypy] -python_version = "3.10" +python_version = "3.14" strict = true + +[[tool.mypy.overrides]] +module = ["wandb", "huggingface_hub"] +ignore_missing_imports = true diff --git a/scripts/compare_benchmarks.py b/scripts/compare_benchmarks.py index 8d59a21..7037567 100644 --- a/scripts/compare_benchmarks.py +++ b/scripts/compare_benchmarks.py @@ -114,7 +114,7 @@ def plot_history(runs: list[dict]) -> None: try: import matplotlib.pyplot as plt except ImportError: - print("matplotlib not installed. Run: pip install matplotlib") + print("matplotlib not installed. Run: uv sync --extra viz") return if not runs: diff --git a/scripts/run_benchmark.py b/scripts/run_benchmark.py index cc47487..e267026 100644 --- a/scripts/run_benchmark.py +++ b/scripts/run_benchmark.py @@ -16,6 +16,7 @@ from felix_agent_sdk.providers.base import BaseProvider from felix_agent_sdk.providers.types import CompletionResult from felix_agent_sdk.visualization import HelixVisualizer + from rle.agents import AGENT_DISPLAY from rle.agents.construction_planner import ConstructionPlanner from rle.agents.defense_commander import DefenseCommander @@ -345,9 +346,25 @@ async def main(args: argparse.Namespace) -> None: helix = HelixConfig.default().to_geometry() scenarios = list_scenarios() provider, config = _build_provider(args) - if args.tick_interval is not None and not args.dry_run: + is_smoke_test = args.smoke_test or args.dry_run + if args.dry_run: + logger.warning("--dry-run is deprecated, use --smoke-test") + if args.tick_interval is not None and not is_smoke_test: config = RLEConfig(**{**config.model_dump(), "tick_interval": args.tick_interval}) - use_mock_rimapi = args.dry_run or args.provider is not None + use_mock_rimapi = (is_smoke_test or args.provider is not None) and not args.docker + + num_runs = getattr(args, "runs", 1) or 1 + + if args.ablation: + print("Ablation study not yet implemented.") + return + + # N >= 4 enforcement + if args.push_hf and num_runs < 4: + print("ERROR: Leaderboard submission requires --runs 4 or higher.") + return + if num_runs < 4 and not use_mock_rimapi: + print(f"WARNING: N={num_runs} runs is below minimum (4) for statistical validity.") ticks_override = _resolve_ticks(args, use_mock_rimapi) output_dir = None @@ -377,154 +394,185 @@ async def main(args: argparse.Namespace) -> None: "ticks_per_scenario": ticks_override, }) - num_runs = getattr(args, "runs", 1) or 1 no_baseline = getattr(args, "no_baseline", False) is_paired = not use_mock_rimapi and not no_baseline results = [] paired_results: list[PairedResult] = [] - async with RimAPIClient(config.rimapi_url) as client: - if use_mock_rimapi: - client._client = httpx.AsyncClient( - transport=_make_mock_transport(), base_url="http://mock", - ) + # Docker lifecycle (optional) + docker_server = None + if args.docker: + from rle.docker import DockerGameServer + docker_server = DockerGameServer( + image=config.docker_image, port=config.docker_port, + ) + await docker_server.start() + config = RLEConfig(**{ + **config.model_dump(), + "rimapi_url": docker_server.url, + }) - for scenario in scenarios: - paired = PairedResult(scenario=scenario.name) if is_paired else None - - for run_id in range(num_runs): - run_label = f" (run {run_id + 1}/{num_runs})" if num_runs > 1 else "" - - # Load save if available (for reproducible initial conditions) - if scenario.save_name and not use_mock_rimapi: - try: - await client.load_game(scenario.save_name) - await asyncio.sleep(GAME_LOAD_WAIT_SECONDS) # Wait for game to load - except Exception as e: - logger.warning("Could not load save %s: %s", scenario.save_name, e) - - # Agent run - print(f"\nRunning: {scenario.name} ({scenario.difficulty}){run_label}...") - result = await _run_scenario( - scenario, config, client, provider, helix, output_dir, - max_ticks_override=ticks_override, - provider_kwargs=provider_kwargs or None, - visualize=args.visualize, - no_think=args.no_think, - parallel=not args.sequential, - no_pause=args.no_pause, - ) - results.append(result) - if paired: - paired.agent_scores.append(result["score"]) - print( - f" -> agent: {result['outcome']} | score={result['score']:.3f} " - f"| {result['ticks']} ticks | {result['elapsed_s']}s " - f"| parse {result['parse_rate']:.0%} ({result['parse_failures']} fail)" + try: + async with RimAPIClient(config.rimapi_url) as client: + if use_mock_rimapi: + client._client = httpx.AsyncClient( + transport=_make_mock_transport(), base_url="http://mock", ) - # Baseline run (reload same save, no agents) - if is_paired: - if scenario.save_name: + for scenario in scenarios: + if docker_server: + await docker_server.restart() + paired = PairedResult(scenario=scenario.name) if is_paired else None + + for run_id in range(num_runs): + run_label = f" (run {run_id + 1}/{num_runs})" if num_runs > 1 else "" + + # Load save if available (for reproducible initial conditions) + if scenario.save_name and not use_mock_rimapi: try: await client.load_game(scenario.save_name) await asyncio.sleep(GAME_LOAD_WAIT_SECONDS) except Exception as e: - logger.warning("Could not reload save: %s", e) + logger.warning("Could not load save %s: %s", scenario.save_name, e) - print(f" baseline{run_label}...") - baseline = await _run_scenario( + # Agent run + print(f"\nRunning: {scenario.name} ({scenario.difficulty}){run_label}...") + result = await _run_scenario( scenario, config, client, provider, helix, output_dir, max_ticks_override=ticks_override, - no_agent=True, + provider_kwargs=provider_kwargs or None, + visualize=args.visualize, + no_think=args.no_think, + parallel=not args.sequential, + no_pause=args.no_pause, + ) + results.append(result) + if paired: + paired.agent_scores.append(result["score"]) + print( + f" -> agent: {result['outcome']} | score={result['score']:.3f} " + f"| {result['ticks']} ticks | {result['elapsed_s']}s " + f"| parse {result['parse_rate']:.0%} ({result['parse_failures']} fail)" ) - paired.baseline_scores.append(baseline["score"]) - print(f" -> baseline: score={baseline['score']:.3f}") - if paired: - paired_results.append(paired) + # Baseline run (reload same save, no agents) + if is_paired: + if scenario.save_name: + try: + await client.load_game(scenario.save_name) + await asyncio.sleep(GAME_LOAD_WAIT_SECONDS) + except Exception as e: + logger.warning("Could not reload save: %s", e) + + print(f" baseline{run_label}...") + baseline = await _run_scenario( + scenario, config, client, provider, helix, output_dir, + max_ticks_override=ticks_override, + no_agent=True, + ) + paired.baseline_scores.append(baseline["score"]) + print(f" -> baseline: score={baseline['score']:.3f}") - # Print results - if is_paired and paired_results: - from rle.scoring.delta import print_paired_leaderboard - print_paired_leaderboard(paired_results, model=args.model, num_runs=num_runs) - else: - _print_leaderboard(results, model=args.model) - - # Build enriched summary with metadata - metadata = collect_metadata() - summary = { - **metadata, - "model": args.model or config.model, - "provider": args.provider or config.provider, - "base_url": args.base_url or None, - "no_think": args.no_think, - "parallel": not args.sequential, - "tick_interval": config.tick_interval, - "ticks_per_scenario": ticks_override, - "num_runs": num_runs, - "paired": is_paired, - "scenarios": results, - } - if is_paired and paired_results: - summary["paired_results"] = [p.to_dict() for p in paired_results] - - # Auto-generate run directory if --output not specified - output_dir = Path(args.output) if args.output else get_run_dir(args.model) - output_dir.mkdir(parents=True, exist_ok=True) - summary_path = output_dir / "benchmark_summary.json" - summary_path.write_text(json.dumps(summary, indent=2, default=str)) - print(f"\nResults exported to {output_dir}/") - - # Only track real benchmark runs (not mock/dry-run JSON compliance tests) - scores = [r.get("score", 0) for r in results] - avg = sum(scores) / len(scores) if scores else 0 - if not use_mock_rimapi: - history_path = append_history(summary) - print(f"History appended to {history_path}") - - is_new_best, prev_score = update_baseline(summary) - if is_new_best: - delta = f"+{avg - prev_score:.3f}" if prev_score else "first run" - print(f"NEW BASELINE: {avg:.3f} ({delta})") - elif prev_score is not None: - print(f"Baseline: {prev_score:.3f} (this run: {avg:.3f})") - else: - print("(dry-run: skipping history/baseline tracking)") + if paired: + paired_results.append(paired) + + # Print results + if is_paired and paired_results: + from rle.scoring.delta import print_paired_leaderboard + print_paired_leaderboard(paired_results, model=args.model, num_runs=num_runs) + else: + _print_leaderboard(results, model=args.model) + + # Build enriched summary with metadata + metadata = collect_metadata() + summary = { + **metadata, + "model": args.model or config.model, + "provider": args.provider or config.provider, + "base_url": args.base_url or None, + "no_think": args.no_think, + "parallel": not args.sequential, + "tick_interval": config.tick_interval, + "ticks_per_scenario": ticks_override, + "num_runs": num_runs, + "paired": is_paired, + "scenarios": results, + } + if is_paired and paired_results: + summary["paired_results"] = [p.to_dict() for p in paired_results] - # W&B logging (optional) - if wandb_logger.enabled: - wandb_logger.log_final_summary( - avg_score=avg, - parse_rate=sum(r.get("parse_successes", 0) for r in results) - / max(1, sum(r["parse_successes"] + r["parse_failures"] for r in results)), - total_time=sum(r.get("elapsed_s", 0) for r in results), - ) - for r in results: - wandb_logger.log_scenario_result(r) - wandb_logger.finish() - print("W&B run logged") - - # HuggingFace Hub push (optional) - if args.push_hf: - hf = HFLogger(enabled=True) - if hf.enabled: - hf.push_results( - history_path=history_path, - baselines_dir=Path("results/baselines"), - run_dir=output_dir, + # Auto-generate run directory if --output not specified + output_dir = Path(args.output) if args.output else get_run_dir(args.model) + output_dir.mkdir(parents=True, exist_ok=True) + summary_path = output_dir / "benchmark_summary.json" + summary_path.write_text(json.dumps(summary, indent=2, default=str)) + print(f"\nResults exported to {output_dir}/") + + # Only track real benchmark runs (not mock/dry-run JSON compliance tests) + scores = [r.get("score", 0) for r in results] + avg = sum(scores) / len(scores) if scores else 0 + if not use_mock_rimapi: + history_path = append_history(summary) + print(f"History appended to {history_path}") + + is_new_best, prev_score = update_baseline(summary) + if is_new_best: + delta = f"+{avg - prev_score:.3f}" if prev_score else "first run" + print(f"NEW BASELINE: {avg:.3f} ({delta})") + elif prev_score is not None: + print(f"Baseline: {prev_score:.3f} (this run: {avg:.3f})") + else: + print("(dry-run: skipping history/baseline tracking)") + + # W&B logging (optional) + if wandb_logger.enabled: + wandb_logger.log_final_summary( + avg_score=avg, + parse_rate=sum(r.get("parse_successes", 0) for r in results) + / max(1, sum(r["parse_successes"] + r["parse_failures"] for r in results)), + total_time=sum(r.get("elapsed_s", 0) for r in results), ) - print("Results pushed to HuggingFace Hub") + for r in results: + wandb_logger.log_scenario_result(r) + wandb_logger.finish() + print("W&B run logged") + + # HuggingFace Hub push (optional) + if args.push_hf: + hf = HFLogger(enabled=True) + if hf.enabled: + hf.push_results( + history_path=history_path, + baselines_dir=Path("results/baselines"), + run_dir=output_dir, + ) + print("Results pushed to HuggingFace Hub") + + finally: + if docker_server: + await docker_server.stop() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run all RLE benchmark scenarios") parser.add_argument("--output", help="Output directory for CSV results") parser.add_argument( - "--dry-run", action="store_true", + "--smoke-test", action="store_true", help="Use mock RIMAPI (combine with --provider for real LLM + fake game)", ) + parser.add_argument( + "--dry-run", action="store_true", + help="(Deprecated, use --smoke-test) Use mock RIMAPI", + ) + parser.add_argument( + "--docker", action="store_true", + help="Use Docker container for headless RimWorld (requires docker/Dockerfile built)", + ) + parser.add_argument( + "--ablation", action="store_true", + help="(WIP) Run ablation study: full benchmark + 7 single-agent-removed runs", + ) parser.add_argument( "--provider", choices=["anthropic", "openai", "local"], help="LLM provider (default: from config)", diff --git a/scripts/run_scenario.py b/scripts/run_scenario.py index 0b057e2..aea751b 100644 --- a/scripts/run_scenario.py +++ b/scripts/run_scenario.py @@ -10,6 +10,7 @@ from felix_agent_sdk.core import HelixConfig from felix_agent_sdk.visualization import HelixVisualizer + from rle.agents import AGENT_DISPLAY from rle.agents.construction_planner import ConstructionPlanner from rle.agents.defense_commander import DefenseCommander @@ -19,6 +20,7 @@ from rle.agents.resource_manager import ResourceManager from rle.agents.social_overseer import SocialOverseer from rle.config import RLEConfig, bridge_openrouter_key +from rle.docker import wait_for_rimapi from rle.orchestration.game_loop import RLEGameLoop from rle.rimapi.client import RimAPIClient from rle.rimapi.sse_client import RimAPISSEClient @@ -142,7 +144,8 @@ async def main(args: argparse.Namespace) -> None: print(f"Loading save: {scenario.save_name}") try: await client.load_game(scenario.save_name) - # Wait for game to fully load — poll until colonists are available + # Wait for RIMAPI to respond, then poll until colonists are loaded + await wait_for_rimapi(config.rimapi_url, timeout=30.0) for _ in range(15): await asyncio.sleep(2) try: diff --git a/scripts/visualize_results.py b/scripts/visualize_results.py index 3cf71b2..e7c232a 100644 --- a/scripts/visualize_results.py +++ b/scripts/visualize_results.py @@ -1,6 +1,6 @@ """Plot score timeseries from RLE benchmark CSV files. -Requires: pip install rimworld-learning-environment[viz] +Requires: uv sync --extra viz """ from __future__ import annotations @@ -27,7 +27,7 @@ def plot_scenario(csv_path: Path, output_path: Path | None = None) -> None: try: import matplotlib.pyplot as plt except ImportError: - print("matplotlib is required: pip install rimworld-learning-environment[viz]") + print("matplotlib is required: uv sync --extra viz") sys.exit(1) data = _load_csv(csv_path) @@ -72,7 +72,7 @@ def plot_comparison(csv_dir: Path, output_path: Path | None = None) -> None: try: import matplotlib.pyplot as plt except ImportError: - print("matplotlib is required: pip install rimworld-learning-environment[viz]") + print("matplotlib is required: uv sync --extra viz") sys.exit(1) csv_files = sorted(csv_dir.glob("*.csv")) diff --git a/src/rle/agents/base_role.py b/src/rle/agents/base_role.py index ca52d15..2c2571b 100644 --- a/src/rle/agents/base_role.py +++ b/src/rle/agents/base_role.py @@ -158,6 +158,8 @@ def __init__( self._no_think: bool = False self._spoke: Spoke | None = None self._pending_events: list[RimAPIEvent] = [] + self._last_usage: dict[str, int] | None = None + self._last_raw_output: str | None = None def set_provider_kwargs(self, **kwargs: Any) -> None: """Set extra kwargs passed to provider.complete() (e.g. extra_body).""" @@ -214,12 +216,15 @@ def _call_provider( messages.append( ChatMessage(role=MessageRole.ASSISTANT, content=""), ) - return self.provider.complete( + result = self.provider.complete( messages, temperature=temperature, max_tokens=max_tokens, **self._provider_kwargs, ) + self._last_raw_output = result.content + self._last_usage = result.usage if hasattr(result, "usage") else None + return result # ------------------------------------------------------------------ # Abstract methods — subclasses must implement diff --git a/src/rle/agents/json_repair.py b/src/rle/agents/json_repair.py index 501e554..770f70e 100644 --- a/src/rle/agents/json_repair.py +++ b/src/rle/agents/json_repair.py @@ -46,7 +46,7 @@ def repair_json(raw: str) -> str: return raw -def try_parse_json(raw: str) -> dict | None: +def try_parse_json(raw: str) -> dict[str, object] | None: """Attempt to repair and parse JSON into a dict. Returns None if parsing fails after repair. diff --git a/src/rle/agents/map_analyst.py b/src/rle/agents/map_analyst.py index cce3ffe..0e21c07 100644 --- a/src/rle/agents/map_analyst.py +++ b/src/rle/agents/map_analyst.py @@ -2,9 +2,12 @@ from __future__ import annotations -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from rle.agents.base_role import RimWorldRoleAgent + +if TYPE_CHECKING: + from felix_agent_sdk.agents.llm_agent import LLMTask from rle.rimapi.schemas import GameState # Bootstrap directive for early game (day < 5). @@ -97,7 +100,7 @@ def _get_role_description(self) -> str: ) def create_position_aware_prompt( - self, task: "LLMTask", # noqa: F821 + self, task: LLMTask, ) -> tuple[str, str]: """Override to inject bootstrap directive for early game.""" system_prompt, user_prompt = super().create_position_aware_prompt(task) diff --git a/src/rle/config.py b/src/rle/config.py index 8a81dae..fa60327 100644 --- a/src/rle/config.py +++ b/src/rle/config.py @@ -40,6 +40,8 @@ class RLEConfig(BaseSettings): helix_preset: str = "default" max_agents: int = 7 log_level: str = "INFO" + docker_image: str = "rle-headless:latest" + docker_port: int = 8765 def get_helix_config(self) -> HelixConfig: """Return the HelixConfig preset matching ``helix_preset``.""" @@ -62,7 +64,7 @@ def get_provider(self) -> BaseProvider: kwargs: dict[str, str] = {"model": self.model} if self.provider_base_url: kwargs["base_url"] = self.provider_base_url - return cls(**kwargs) + return cls(**kwargs) # type: ignore[arg-type] # subclasses accept kwargs def bridge_openrouter_key(config: RLEConfig) -> None: diff --git a/src/rle/docker.py b/src/rle/docker.py new file mode 100644 index 0000000..f475d70 --- /dev/null +++ b/src/rle/docker.py @@ -0,0 +1,121 @@ +"""Docker container lifecycle management for headless RimWorld benchmarks.""" + +from __future__ import annotations + +import asyncio +import logging +from subprocess import DEVNULL +from types import TracebackType + +import httpx + +logger = logging.getLogger(__name__) + +CONTAINER_NAME = "rle-benchmark" +DEFAULT_IMAGE = "rle-headless:latest" +DEFAULT_PORT = 8765 +HEALTH_TIMEOUT = 120.0 +HEALTH_INTERVAL = 5.0 + + +async def _probe_rimapi(url: str, timeout: float = 5.0) -> bool: + """Check if RIMAPI is responsive at the given URL.""" + try: + async with httpx.AsyncClient() as client: + resp = await client.get(f"{url}/api/v1/game/state", timeout=timeout) + return resp.status_code < 500 + except (httpx.HTTPError, OSError): + return False + + +async def wait_for_rimapi(url: str, timeout: float = HEALTH_TIMEOUT) -> None: + """Poll RIMAPI until responsive. Shared by Docker and manual setups. + + Raises TimeoutError if RIMAPI doesn't respond within *timeout* seconds. + """ + elapsed = 0.0 + while elapsed < timeout: + if await _probe_rimapi(url): + logger.info("RIMAPI responsive at %s", url) + return + await asyncio.sleep(HEALTH_INTERVAL) + elapsed += HEALTH_INTERVAL + msg = f"RIMAPI not responsive at {url} after {timeout}s" + raise TimeoutError(msg) + + +async def _run( + *args: str, + check: bool = True, +) -> None: + """Run a Docker CLI command asynchronously.""" + proc = await asyncio.create_subprocess_exec( + "docker", *args, + stdout=DEVNULL, + stderr=asyncio.subprocess.PIPE, + ) + _, stderr = await proc.communicate() + if check and proc.returncode != 0: + err = stderr.decode().strip() if stderr else "unknown error" + msg = f"docker {' '.join(args)} failed: {err}" + raise RuntimeError(msg) + + +class DockerGameServer: + """Manages HeadlessRim Docker container for automated benchmarks.""" + + def __init__( + self, + image: str = DEFAULT_IMAGE, + port: int = DEFAULT_PORT, + container_name: str = CONTAINER_NAME, + ) -> None: + self._image = image + self._port = port + self._container_name = container_name + + @property + def url(self) -> str: + """RIMAPI base URL for this container.""" + return f"http://localhost:{self._port}" + + async def start(self) -> None: + """Start container and wait for RIMAPI healthcheck.""" + logger.info("Starting %s from image %s", self._container_name, self._image) + # Remove stale container with same name if it exists + await _run("rm", "-f", self._container_name, check=False) + await _run( + "run", "-d", "--rm", + "-p", f"{self._port}:8765", + "--name", self._container_name, + "--shm-size=1g", + self._image, + ) + await wait_for_rimapi(self.url) + + async def stop(self) -> None: + """Stop container. --rm flag auto-removes it after stop.""" + logger.info("Stopping %s", self._container_name) + await _run("stop", self._container_name, check=False) + + async def restart(self) -> None: + """Restart for clean game state between scenarios.""" + logger.info("Restarting %s", self._container_name) + await _run("restart", self._container_name) + await wait_for_rimapi(self.url) + + async def is_healthy(self) -> bool: + """Check if RIMAPI is responsive.""" + return await _probe_rimapi(self.url) + + async def __aenter__(self) -> DockerGameServer: + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.stop() diff --git a/src/rle/orchestration/action_executor.py b/src/rle/orchestration/action_executor.py index ec458d2..f0ed2a1 100644 --- a/src/rle/orchestration/action_executor.py +++ b/src/rle/orchestration/action_executor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from pydantic import BaseModel, ConfigDict @@ -92,7 +92,8 @@ async def _dispatch(self, action: Action, endpoint: str) -> None: logger.debug("Unknown endpoint: %s", endpoint) return - await self._client.call(catalog_entry["method"], catalog_entry["path"], json=params) + entry = cast(dict[str, str], catalog_entry) + await self._client.call(entry["method"], entry["path"], json=params) # -- Specialized handlers (parameter mapping for complex DTOs) ----------- diff --git a/src/rle/orchestration/game_loop.py b/src/rle/orchestration/game_loop.py index bb196b7..dfb9a7c 100644 --- a/src/rle/orchestration/game_loop.py +++ b/src/rle/orchestration/game_loop.py @@ -19,6 +19,7 @@ from rle.orchestration.action_resolver import ActionResolver from rle.orchestration.state_manager import GameStateManager from rle.rimapi.client import RimAPIClient +from rle.rimapi.schemas import GameState from rle.rimapi.sse_client import RimAPISSEClient from rle.scenarios.evaluator import EvaluationResult, ScenarioEvaluator from rle.scoring.composite import CompositeScorer, ScoreSnapshot @@ -91,7 +92,7 @@ def __init__( self._parse_successes = 0 self._parse_failures = 0 self._log_dir: Path | None = None - self._deliberation_log: list[dict] = [] + self._deliberation_log: list[dict[str, object]] = [] self._parallel = parallel self._last_phase: str = "" self._dashboard_export_dir = dashboard_export_dir @@ -262,11 +263,11 @@ def _export_tick_json( json.dumps(data, indent=2), ) - def _update_metric_context(self, result: TickResult, state: object) -> None: + def _update_metric_context(self, result: TickResult, state: GameState) -> None: """Append tick data to metric context for scoring history.""" self._metric_context.tick_results.append(result) self._metric_context.state_history.append(state) - for threat in state.threats: # type: ignore[attr-defined] + for threat in state.threats: seen_ids = {t.threat_id for t in self._metric_context.threats_seen} if threat.threat_id not in seen_ids: self._metric_context.threats_seen.append(threat) diff --git a/src/rle/rimapi/client.py b/src/rle/rimapi/client.py index 03cffb6..202c77e 100644 --- a/src/rle/rimapi/client.py +++ b/src/rle/rimapi/client.py @@ -3,7 +3,9 @@ from __future__ import annotations import time +from collections.abc import Callable from types import TracebackType +from typing import Any import httpx @@ -80,7 +82,7 @@ def client(self) -> httpx.AsyncClient: # Internal helpers # ------------------------------------------------------------------ - async def _get(self, path: str) -> dict: + async def _get(self, path: str) -> Any: """Perform a GET request, unwrap RIMAPI envelope, return data payload.""" try: resp = await self.client.get(path) @@ -97,13 +99,16 @@ async def _get(self, path: str) -> dict: return body["data"] return body - async def call(self, method: str, path: str, json: dict | None = None) -> dict: + async def call( + self, method: str, path: str, json: dict[str, Any] | None = None, + ) -> dict[str, Any]: """Generic endpoint call — used by the dynamic action dispatcher.""" if method.upper() == "GET": - return await self._get(path) + result: dict[str, Any] = await self._get(path) + return result return await self._post(path, json=json) - async def _post(self, path: str, json: dict | None = None) -> dict: + async def _post(self, path: str, json: dict[str, Any] | None = None) -> dict[str, Any]: """Perform a POST request and return parsed JSON.""" try: resp = await self.client.post(path, json=json or {}) @@ -116,7 +121,8 @@ async def _post(self, path: str, json: dict | None = None) -> dict: raise RimAPIResponseError(resp.status_code, resp.text) if resp.status_code == 204: return {} - return resp.json() + result: dict[str, Any] = resp.json() + return result # ------------------------------------------------------------------ # Read endpoints @@ -130,7 +136,7 @@ async def _post(self, path: str, json: dict | None = None) -> dict: # ------------------------------------------------------------------ @staticmethod - def _adapt_colonist(raw: dict) -> dict: + def _adapt_colonist(raw: dict[str, Any]) -> dict[str, Any]: """Map upstream detailed PawnDto → ColonistData fields. Handles three formats: @@ -196,7 +202,7 @@ def _adapt_colonist(raw: dict) -> dict: } @staticmethod - def _adapt_colony(raw: dict) -> dict: + def _adapt_colony(raw: dict[str, Any]) -> dict[str, Any]: """Map upstream GameStateDto → ColonyData fields. Handles both upstream (game_tick, colony_wealth, colonist_count) @@ -215,7 +221,7 @@ def _adapt_colony(raw: dict) -> dict: } @staticmethod - def _adapt_research(raw: dict) -> dict: + def _adapt_research(raw: dict[str, Any]) -> dict[str, Any]: """Map upstream ResearchSummaryDto → ResearchData fields.""" if "current_project" in raw: return raw @@ -418,16 +424,18 @@ async def get_farm_summary(self, map_id: int = 0) -> FarmSummary | None: crops: dict[str, int] = {} for crop in data.get("crop_types", []): if isinstance(crop, dict): - name = crop.get("def_name", crop.get("label", "unknown")) - crops[name] = int(crop.get("count", crop.get("total", 1))) + name: str = str(crop.get("def_name", crop.get("label", "unknown"))) + count_val = crop.get("count", crop.get("total", 1)) + crops[name] = int(count_val) if count_val is not None else 1 elif isinstance(crop, str): crops[crop] = crops.get(crop, 0) + 1 + total_zones = data.get("total_growing_zones", 0) + planted = data.get("total_plants", data.get("planted_cells", 0)) + harvestable = data.get("total_expected_yield", data.get("harvestable_cells", 0)) return FarmSummary( - total_growing_zones=int(data.get("total_growing_zones", 0)), - planted_cells=int(data.get("total_plants", data.get("planted_cells", 0))), - harvestable_cells=int( - data.get("total_expected_yield", data.get("harvestable_cells", 0)), - ), + total_growing_zones=int(total_zones) if total_zones is not None else 0, + planted_cells=int(planted) if planted is not None else 0, + harvestable_cells=int(harvestable) if harvestable is not None else 0, crops=crops, ) except (RimAPIResponseError, RimAPIConnectionError): @@ -501,7 +509,7 @@ def _is_fertile(x: int, z: int) -> bool: # Find best rectangular areas near colony center using expanding search def _find_clear_rect( - center_x: int, center_z: int, min_size: int, check_fn: callable, + center_x: int, center_z: int, min_size: int, check_fn: Callable[[int, int], bool], ) -> AreaRect | None: """Search outward from center for a clear rectangle.""" for radius in range(0, 60, 3): @@ -524,12 +532,18 @@ def _find_clear_rect( # Find water areas near center (for avoidance) water_areas: list[AreaRect] = [] scan_range = 40 - water_x_min = water_x_max = water_z_min = water_z_max = None + water_x_min: int | None = None + water_x_max: int | None = None + water_z_min: int | None = None + water_z_max: int | None = None for dz in range(-scan_range, scan_range + 1): for dx in range(-scan_range, scan_range + 1): x, z = cx + dx, cz + dz if _cell(x, z) == 1: - if water_x_min is None: + if ( + water_x_min is None or water_x_max is None + or water_z_min is None or water_z_max is None + ): water_x_min = water_x_max = x water_z_min = water_z_max = z else: @@ -537,7 +551,10 @@ def _find_clear_rect( water_x_max = max(water_x_max, x) water_z_min = min(water_z_min, z) water_z_max = max(water_z_max, z) - if water_x_min is not None: + if ( + water_x_min is not None and water_x_max is not None + and water_z_min is not None and water_z_max is not None + ): water_areas.append(AreaRect( x1=water_x_min, z1=water_z_min, x2=water_x_max, z2=water_z_max, @@ -642,7 +659,7 @@ async def unforbid_all_items(self, map_id: int = 0) -> int: """Unforbid all forbidden items on the map. Returns count unforbidden.""" try: data = await self._get(f"/api/v1/map/things?map_id={map_id}") - things = data if isinstance(data, list) else [] + things: list[dict[str, Any]] = data if isinstance(data, list) else [] forbidden_ids = [ t["thing_id"] for t in things if t.get("is_forbidden", False) @@ -669,13 +686,13 @@ def _int_id(colonist_id: str) -> int: except (ValueError, TypeError): return 0 - async def save_game(self, name: str) -> dict: + async def save_game(self, name: str) -> dict[str, Any]: """Save the current game state for benchmark reproducibility.""" return await self._post("/api/v1/game/save", json={"file_name": name}) async def load_game( self, name: str, check_version: bool = False, skip_mod_mismatch: bool = True, - ) -> dict: + ) -> dict[str, Any]: """Load a previously saved game state.""" return await self._post("/api/v1/game/load", json={ "file_name": name, @@ -683,14 +700,14 @@ async def load_game( "skip_mod_mismatch": skip_mod_mismatch, }) - async def pause_game(self) -> dict: + async def pause_game(self) -> dict[str, Any]: return await self._post("/api/v1/game/speed?speed=0") - async def unpause_game(self, speed: int = 3) -> dict: + async def unpause_game(self, speed: int = 3) -> dict[str, Any]: """Unpause at given speed (1=normal, 2=fast, 3=very fast).""" return await self._post(f"/api/v1/game/speed?speed={speed}") - async def draft_colonist(self, colonist_id: str, draft: bool) -> dict: + async def draft_colonist(self, colonist_id: str, draft: bool) -> dict[str, Any]: return await self._post( "/api/v1/pawn/edit/status", json={"pawn_id": self._int_id(colonist_id), "is_drafted": draft}, @@ -698,7 +715,7 @@ async def draft_colonist(self, colonist_id: str, draft: bool) -> dict: async def set_work_priorities( self, colonist_id: str, priorities: dict[str, int], - ) -> dict: + ) -> dict[str, Any]: """Set work priorities one at a time via the singular endpoint. The bulk endpoint (/api/v1/colonists/work-priority) has a @@ -706,7 +723,7 @@ async def set_work_priorities( Use the singular endpoint which has explicit [JsonProperty] attrs. """ pid = self._int_id(colonist_id) - last_result: dict = {} + last_result: dict[str, Any] = {} for work, pri in priorities.items(): last_result = await self._post( "/api/v1/colonist/work-priority", @@ -714,7 +731,7 @@ async def set_work_priorities( ) return last_result - async def place_blueprint(self, blueprint: dict) -> dict: + async def place_blueprint(self, blueprint: dict[str, Any]) -> dict[str, Any]: """Place a blueprint using the full PasteAreaRequestDto format. The blueprint dict must contain: map_id, position, blueprint (with @@ -732,7 +749,7 @@ async def place_building( stuff_def: str = "WoodLog", rotation: int = 0, map_id: int = 0, - ) -> dict: + ) -> dict[str, Any]: """Place a single building blueprint at (x, z). Wraps the PasteAreaRequestDto with a 1x1 blueprint grid. @@ -757,7 +774,7 @@ async def place_building( "clear_obstacles": True, }) - async def move_colonist(self, colonist_id: str, x: int, z: int) -> dict: + async def move_colonist(self, colonist_id: str, x: int, z: int) -> dict[str, Any]: return await self._post( "/api/v1/pawn/edit/position", json={ @@ -768,7 +785,7 @@ async def move_colonist(self, colonist_id: str, x: int, z: int) -> dict: async def set_time_assignment( self, colonist_id: str, hour: int, assignment: str, - ) -> dict: + ) -> dict[str, Any]: return await self._post( "/api/v1/colonist/time-assignment", json={"pawn_id": self._int_id(colonist_id), "hour": hour, "assignment": assignment}, @@ -782,7 +799,7 @@ async def designate_area( z1: int, x2: int, z2: int, - ) -> dict: + ) -> dict[str, Any]: return await self._post( "/api/v1/order/designate/area", json={ @@ -797,7 +814,7 @@ async def designate_area( # Write endpoints — RLE fork (AppSprout-dev/RIMAPI:rle-testing) # ------------------------------------------------------------------ - async def set_research_target(self, project: str, force: bool = False) -> dict: + async def set_research_target(self, project: str, force: bool = False) -> dict[str, Any]: if not project: return {"success": False, "skipped": "empty project name"} url = f"/api/v1/research/target?name={project}" @@ -805,13 +822,14 @@ async def set_research_target(self, project: str, force: bool = False) -> dict: url += "&force=true" return await self._post(url) - async def stop_research(self) -> dict: + async def stop_research(self) -> dict[str, Any]: return await self._post("/api/v1/research/stop") - async def get_endpoints(self) -> list[dict]: + async def get_endpoints(self) -> list[dict[str, Any]]: """Discover all registered API routes from RIMAPI.""" try: - return await self._get("/api/v1/dev/endpoints") + data: list[dict[str, Any]] = await self._get("/api/v1/dev/endpoints") + return data except (RimAPIResponseError, RimAPIConnectionError): return [] @@ -821,15 +839,15 @@ async def set_colonist_job( job: str, target_thing_id: int | None = None, target_position: tuple[int, int] | None = None, - ) -> dict: - body: dict = {"pawn_id": self._int_id(colonist_id), "job_def": job} + ) -> dict[str, Any]: + body: dict[str, Any] = {"pawn_id": self._int_id(colonist_id), "job_def": job} if target_thing_id is not None: body["target_thing_id"] = target_thing_id if target_position is not None: body["target_position"] = {"x": target_position[0], "y": 0, "z": target_position[1]} return await self._post("/api/v1/pawn/job", json=body) - async def toggle_power(self, building_id: int, power_on: bool) -> dict: + async def toggle_power(self, building_id: int, power_on: bool) -> dict[str, Any]: return await self._post( f"/api/v1/map/building/power?buildingId={building_id}" f"&powerOn={str(power_on).lower()}", @@ -854,7 +872,7 @@ async def create_growing_zone( z1: int, x2: int, z2: int, - ) -> dict: + ) -> dict[str, Any]: return await self._post( "/api/v1/map/zone/growing", json={ @@ -876,9 +894,9 @@ async def create_stockpile_zone( priority: int = 3, allowed_item_defs: list[str] | None = None, allowed_item_categories: list[str] | None = None, - ) -> dict: + ) -> dict[str, Any]: """Create a stockpile zone with optional item filtering.""" - body: dict = { + body: dict[str, Any] = { "map_id": map_id, "point_a": {"x": x1, "y": 0, "z": z1}, "point_b": {"x": x2, "y": 0, "z": z2}, @@ -895,16 +913,16 @@ async def create_stockpile_zone( async def assign_bed_rest( self, patient_id: str, bed_building_id: int | None = None, - ) -> dict: - body: dict = {"patient_pawn_id": self._int_id(patient_id)} + ) -> dict[str, Any]: + body: dict[str, Any] = {"patient_pawn_id": self._int_id(patient_id)} if bed_building_id is not None: body["bed_building_id"] = bed_building_id return await self._post("/api/v1/pawn/medical/bed-rest", json=body) async def administer_medicine( self, patient_id: str, doctor_id: str | None = None, - ) -> dict: - body: dict = {"patient_pawn_id": self._int_id(patient_id)} + ) -> dict[str, Any]: + body: dict[str, Any] = {"patient_pawn_id": self._int_id(patient_id)} if doctor_id is not None: body["doctor_pawn_id"] = self._int_id(doctor_id) return await self._post("/api/v1/pawn/medical/tend", json=body) diff --git a/src/rle/rimapi/sse_client.py b/src/rle/rimapi/sse_client.py index 732be26..e289f17 100644 --- a/src/rle/rimapi/sse_client.py +++ b/src/rle/rimapi/sse_client.py @@ -90,7 +90,7 @@ def drain(self) -> list[RimAPIEvent]: def drain_by_type(self, *event_types: str) -> list[RimAPIEvent]: """Drain only events matching the given types, leave others buffered.""" matched = [] - remaining = deque(maxlen=self._buffer.maxlen) + remaining: deque[RimAPIEvent] = deque(maxlen=self._buffer.maxlen) for event in self._buffer: if event.event_type in event_types: matched.append(event) diff --git a/src/rle/scenarios/definitions/01_crashlanded_survival.yaml b/src/rle/scenarios/definitions/01_crashlanded_survival.yaml index 65a0ac8..2e5962a 100644 --- a/src/rle/scenarios/definitions/01_crashlanded_survival.yaml +++ b/src/rle/scenarios/definitions/01_crashlanded_survival.yaml @@ -28,3 +28,5 @@ scoring_weights: research: 0.05 self_sufficiency: 0.15 efficiency: 0.05 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/definitions/02_first_winter.yaml b/src/rle/scenarios/definitions/02_first_winter.yaml index 9db62b0..91bd359 100644 --- a/src/rle/scenarios/definitions/02_first_winter.yaml +++ b/src/rle/scenarios/definitions/02_first_winter.yaml @@ -31,3 +31,5 @@ scoring_weights: research: 0.05 threat_response: 0.10 efficiency: 0.05 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/definitions/03_toxic_fallout.yaml b/src/rle/scenarios/definitions/03_toxic_fallout.yaml index 1885643..a3ac460 100644 --- a/src/rle/scenarios/definitions/03_toxic_fallout.yaml +++ b/src/rle/scenarios/definitions/03_toxic_fallout.yaml @@ -31,3 +31,5 @@ scoring_weights: research: 0.05 threat_response: 0.05 efficiency: 0.05 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/definitions/04_raid_defense.yaml b/src/rle/scenarios/definitions/04_raid_defense.yaml index a11fd45..dac69d5 100644 --- a/src/rle/scenarios/definitions/04_raid_defense.yaml +++ b/src/rle/scenarios/definitions/04_raid_defense.yaml @@ -31,3 +31,5 @@ scoring_weights: research: 0.05 self_sufficiency: 0.10 efficiency: 0.10 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/definitions/05_plague_response.yaml b/src/rle/scenarios/definitions/05_plague_response.yaml index f0a49b9..7c7ac55 100644 --- a/src/rle/scenarios/definitions/05_plague_response.yaml +++ b/src/rle/scenarios/definitions/05_plague_response.yaml @@ -31,3 +31,5 @@ scoring_weights: research: 0.05 self_sufficiency: 0.10 efficiency: 0.10 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/definitions/06_ship_launch.yaml b/src/rle/scenarios/definitions/06_ship_launch.yaml index c83ba73..9b9a8c2 100644 --- a/src/rle/scenarios/definitions/06_ship_launch.yaml +++ b/src/rle/scenarios/definitions/06_ship_launch.yaml @@ -25,3 +25,5 @@ scoring_weights: mood: 0.10 threat_response: 0.05 efficiency: 0.05 + coordination: 0.0 + communication_efficiency: 0.0 diff --git a/src/rle/scenarios/evaluator.py b/src/rle/scenarios/evaluator.py index db4ecc9..b33f11a 100644 --- a/src/rle/scenarios/evaluator.py +++ b/src/rle/scenarios/evaluator.py @@ -98,4 +98,4 @@ def _check_condition( comparator = _OPERATORS.get(cond.operator) if comparator is None: return False - return comparator(actual, cond.value) # type: ignore[operator] + return comparator(actual, cond.value) # type: ignore[no-any-return] diff --git a/src/rle/scoring/__init__.py b/src/rle/scoring/__init__.py index 18493ca..ab74e8a 100644 --- a/src/rle/scoring/__init__.py +++ b/src/rle/scoring/__init__.py @@ -1,13 +1,17 @@ -"""Scoring system — metrics, composite scorer, time-series recording.""" +"""Scoring system — metrics, composite scorer, time-series recording, bootstrap.""" +from rle.scoring.bootstrap import BootstrapCI, bootstrap_ci, bootstrap_paired_delta from rle.scoring.composite import CompositeScorer, ScoreSnapshot from rle.scoring.metrics import ALL_METRICS, MetricContext from rle.scoring.recorder import TimeSeriesRecorder __all__ = [ "ALL_METRICS", + "BootstrapCI", "CompositeScorer", "MetricContext", "ScoreSnapshot", "TimeSeriesRecorder", + "bootstrap_ci", + "bootstrap_paired_delta", ] diff --git a/src/rle/scoring/bootstrap.py b/src/rle/scoring/bootstrap.py new file mode 100644 index 0000000..b2d03be --- /dev/null +++ b/src/rle/scoring/bootstrap.py @@ -0,0 +1,99 @@ +"""Bootstrap confidence intervals for RLE benchmark statistics.""" + +from __future__ import annotations + +import math +import random + +from pydantic import BaseModel, ConfigDict + + +class BootstrapCI(BaseModel): + """Bootstrap confidence interval at a configurable confidence level.""" + + model_config = ConfigDict(frozen=True) + + mean: float + ci_lower: float # lower percentile bound + ci_upper: float # upper percentile bound + std: float + n_samples: int + n_bootstrap: int = 10_000 + + +def bootstrap_ci( + values: list[float], + n_bootstrap: int = 10_000, + ci: float = 0.95, + seed: int | None = None, +) -> BootstrapCI: + """Compute bootstrap confidence interval using percentile method. + + Uses only stdlib random.choices() — no scipy/numpy dependency (see ADR-003). + + Args: + values: Sample values (need at least 1) + n_bootstrap: Number of bootstrap resamples (default 10,000) + ci: Confidence level (default 0.95) + seed: Optional random seed for reproducibility + + Raises: + ValueError: If values is empty. + """ + if not values: + raise ValueError("values must not be empty") + + n = len(values) + mean = sum(values) / n + + if n == 1: + return BootstrapCI( + mean=mean, + ci_lower=mean, + ci_upper=mean, + std=0.0, + n_samples=n, + n_bootstrap=n_bootstrap, + ) + + # Sample std (ddof=1) — consistent with PairedResult._std in delta.py + std = math.sqrt(sum((x - mean) ** 2 for x in values) / (n - 1)) + + rng = random.Random(seed) + boot_means = sorted(sum(rng.choices(values, k=n)) / n for _ in range(n_bootstrap)) + + alpha = 1.0 - ci + lower_idx = max(0, min(int(alpha / 2 * n_bootstrap), n_bootstrap - 1)) + upper_idx = max(0, min(int(math.ceil((1.0 - alpha / 2) * n_bootstrap)) - 1, n_bootstrap - 1)) + + return BootstrapCI( + mean=mean, + ci_lower=boot_means[lower_idx], + ci_upper=boot_means[upper_idx], + std=std, + n_samples=n, + n_bootstrap=n_bootstrap, + ) + + +def bootstrap_paired_delta( + agent_scores: list[float], + baseline_scores: list[float], + n_bootstrap: int = 10_000, + ci: float = 0.95, + seed: int | None = None, +) -> BootstrapCI: + """Bootstrap CI for the agent-baseline delta. + + Resamples paired differences to get CI on the performance gap. + + Args: + agent_scores: Scores from the agent run. + baseline_scores: Scores from the baseline run. + n_bootstrap: Number of bootstrap resamples. + ci: Confidence level (default 0.95). + seed: Optional random seed for reproducibility. + """ + n = min(len(agent_scores), len(baseline_scores)) + deltas = [agent_scores[i] - baseline_scores[i] for i in range(n)] + return bootstrap_ci(deltas, n_bootstrap=n_bootstrap, ci=ci, seed=seed) diff --git a/src/rle/scoring/composite.py b/src/rle/scoring/composite.py index d262497..3dfaa13 100644 --- a/src/rle/scoring/composite.py +++ b/src/rle/scoring/composite.py @@ -16,6 +16,10 @@ "research": 0.10, "self_sufficiency": 0.10, "efficiency": 0.05, + # Process metrics: weight 0.0 until game loop populates MetricContext counters. + # Target weights once wired: coordination=0.12, communication_efficiency=0.08. + "coordination": 0.0, + "communication_efficiency": 0.0, } diff --git a/src/rle/scoring/delta.py b/src/rle/scoring/delta.py index c227dcb..72c9680 100644 --- a/src/rle/scoring/delta.py +++ b/src/rle/scoring/delta.py @@ -4,6 +4,9 @@ import math from dataclasses import dataclass, field +from functools import cached_property + +from rle.scoring.bootstrap import BootstrapCI, bootstrap_ci, bootstrap_paired_delta @dataclass @@ -77,14 +80,32 @@ def significance(self) -> str: return "*" return "" - def to_dict(self) -> dict: + @cached_property + def agent_ci(self) -> BootstrapCI | None: + """Bootstrap 95% CI for agent scores. Cached after first access.""" + if len(self.agent_scores) < 2: + return None + return bootstrap_ci(self.agent_scores) + + @cached_property + def delta_ci(self) -> BootstrapCI | None: + """Bootstrap CI for agent-baseline delta. Cached after first access.""" + if len(self.agent_scores) < 2 or len(self.baseline_scores) < 2: + return None + return bootstrap_paired_delta(self.agent_scores, self.baseline_scores) + + def to_dict(self) -> dict[str, object]: + ci = self.agent_ci + dci = self.delta_ci return { "scenario": self.scenario, "agent_mean": round(self.agent_mean, 4), "agent_std": round(self.agent_std, 4), + "agent_ci": [round(ci.ci_lower, 4), round(ci.ci_upper, 4)] if ci else None, "baseline_mean": round(self.baseline_mean, 4), "baseline_std": round(self.baseline_std, 4), "delta": round(self.delta, 4), + "delta_ci": [round(dci.ci_lower, 4), round(dci.ci_upper, 4)] if dci else None, "effect_size": round(self.effect_size, 2), "p_value": round(self.p_value, 4) if self.p_value is not None else None, "n_agent": len(self.agent_scores), diff --git a/src/rle/scoring/metrics.py b/src/rle/scoring/metrics.py index ba0fc35..7185c88 100644 --- a/src/rle/scoring/metrics.py +++ b/src/rle/scoring/metrics.py @@ -21,6 +21,11 @@ class MetricContext: threats_seen: list[ThreatData] = field(default_factory=list) first_draft_tick: dict[str, int] = field(default_factory=dict) initial_wealth: float = 0.0 + # Process metrics (populated by game loop after conflict resolution) + conflicts_total: int = 0 + conflicts_resolved: int = 0 + messages_sent: int = 0 + messages_acted_on: int = 0 def survival(state: GameState, ctx: MetricContext) -> float: @@ -98,6 +103,20 @@ def efficiency(state: GameState, ctx: MetricContext) -> float: return sum(rates) / len(rates) +def coordination(state: GameState, ctx: MetricContext) -> float: + """Ratio of conflicts resolved peacefully. 1.0 = no conflicts or all resolved.""" + if ctx.conflicts_total == 0: + return 1.0 + return min(1.0, ctx.conflicts_resolved / ctx.conflicts_total) + + +def communication_efficiency(state: GameState, ctx: MetricContext) -> float: + """Ratio of inter-agent messages that led to action changes. 1.0 = all useful.""" + if ctx.messages_sent == 0: + return 1.0 + return min(1.0, ctx.messages_acted_on / ctx.messages_sent) + + ALL_METRICS = { "survival": survival, "threat_response": threat_response, @@ -107,4 +126,6 @@ def efficiency(state: GameState, ctx: MetricContext) -> float: "research": research, "self_sufficiency": self_sufficiency, "efficiency": efficiency, + "coordination": coordination, + "communication_efficiency": communication_efficiency, } diff --git a/src/rle/scoring/recorder.py b/src/rle/scoring/recorder.py index 94921d1..cf26f78 100644 --- a/src/rle/scoring/recorder.py +++ b/src/rle/scoring/recorder.py @@ -33,7 +33,7 @@ def to_csv(self, path: str | Path) -> None: row["composite"] = round(snap.composite, 4) writer.writerow(row) - def to_dicts(self) -> list[dict]: + def to_dicts(self) -> list[dict[str, object]]: """Return list of dicts for programmatic access.""" return [ {"tick": s.tick, "day": s.day, **s.metrics, "composite": s.composite} diff --git a/src/rle/tracking/__init__.py b/src/rle/tracking/__init__.py index acae540..4d421ea 100644 --- a/src/rle/tracking/__init__.py +++ b/src/rle/tracking/__init__.py @@ -1,11 +1,16 @@ -"""Benchmark tracking — local JSONL, W&B, HuggingFace Hub.""" +"""Benchmark tracking — local JSONL, W&B, HuggingFace Hub, event logging.""" +from rle.tracking.event_log import Event, EventLog, EventType, RunSummary from rle.tracking.hf_logger import HFLogger from rle.tracking.history import append_history, get_run_dir, load_history, update_baseline from rle.tracking.wandb_logger import WandBLogger __all__ = [ + "Event", + "EventLog", + "EventType", "HFLogger", + "RunSummary", "WandBLogger", "append_history", "get_run_dir", diff --git a/src/rle/tracking/cost_tracker.py b/src/rle/tracking/cost_tracker.py new file mode 100644 index 0000000..4c12af8 --- /dev/null +++ b/src/rle/tracking/cost_tracker.py @@ -0,0 +1,117 @@ +"""Cost tracking for RLE benchmarks with real-time OpenRouter pricing.""" + +from __future__ import annotations + +import logging +import time + +import httpx +from pydantic import BaseModel, ConfigDict + +logger = logging.getLogger(__name__) + +OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models" + + +class TokenUsage(BaseModel): + """Token usage from a single LLM call.""" + + model_config = ConfigDict(frozen=True) + + prompt_tokens: int + completion_tokens: int + + @property + def total_tokens(self) -> int: + return self.prompt_tokens + self.completion_tokens + + +class CostSnapshot(BaseModel): + """Cumulative cost at a point in time.""" + + model_config = ConfigDict(frozen=True) + + total_prompt_tokens: int + total_completion_tokens: int + total_tokens: int + estimated_cost_usd: float + wall_time_s: float + num_calls: int + + +class CostTracker: + """Accumulates token usage and estimates cost across a benchmark run.""" + + def __init__( + self, + model: str, + prompt_price: float = 0.0, + completion_price: float = 0.0, + ) -> None: + self._prompt_price = prompt_price + self._completion_price = completion_price + self._total_prompt = 0 + self._total_completion = 0 + self._num_calls = 0 + self._start_time = time.monotonic() + + def record(self, usage: TokenUsage) -> None: + """Record token usage from one LLM call.""" + self._total_prompt += usage.prompt_tokens + self._total_completion += usage.completion_tokens + self._num_calls += 1 + + def record_raw(self, prompt_tokens: int, completion_tokens: int) -> None: + """Record from raw token counts (convenience for dict-based usage).""" + self.record(TokenUsage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens)) + + def snapshot(self) -> CostSnapshot: + """Current cumulative cost.""" + total = self._total_prompt + self._total_completion + cost = ( + self._total_prompt * self._prompt_price + + self._total_completion * self._completion_price + ) + return CostSnapshot( + total_prompt_tokens=self._total_prompt, + total_completion_tokens=self._total_completion, + total_tokens=total, + estimated_cost_usd=round(cost, 6), + wall_time_s=round(time.monotonic() - self._start_time, 2), + num_calls=self._num_calls, + ) + + +async def fetch_pricing(model: str, timeout: float = 10.0) -> tuple[float, float]: + """Fetch per-token pricing from OpenRouter's public API. + + GET https://openrouter.ai/api/v1/models (no auth required) + Returns (prompt_price_per_token, completion_price_per_token). + Falls back to (0.0, 0.0) if model not found or API unreachable. + + The API returns pricing like: + {"pricing": {"prompt": "0.000005", "completion": "0.000025"}} + These are USD per token (strings). + """ + try: + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.get(OPENROUTER_MODELS_URL) + resp.raise_for_status() + data = resp.json() + for m in data.get("data", []): + if m.get("id") == model: + pricing = m.get("pricing", {}) + prompt = float(pricing.get("prompt", "0")) + completion = float(pricing.get("completion", "0")) + return (prompt, completion) + logger.warning("Model %r not found in OpenRouter pricing, using $0.00", model) + return (0.0, 0.0) + except Exception: + logger.warning("Could not fetch OpenRouter pricing, using $0.00", exc_info=True) + return (0.0, 0.0) + + +async def create_cost_tracker(model: str) -> CostTracker: + """Create a CostTracker with pricing fetched from OpenRouter.""" + prompt_price, completion_price = await fetch_pricing(model) + return CostTracker(model, prompt_price, completion_price) diff --git a/src/rle/tracking/event_log.py b/src/rle/tracking/event_log.py new file mode 100644 index 0000000..3fe5aed --- /dev/null +++ b/src/rle/tracking/event_log.py @@ -0,0 +1,156 @@ +"""Structured event logging for RLE benchmark observability. + +Produces a single append-only JSONL file per benchmark run capturing every +significant event. This is the offline source of truth — works everywhere +with no dependencies. W&B Weave provides optional rich visualization on top. +""" + +from __future__ import annotations + +import time +from collections import Counter +from enum import Enum +from pathlib import Path +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class EventType(str, Enum): + """Categories of benchmark events.""" + + TICK_START = "tick_start" + STATE_REFRESH = "state_refresh" + DELIBERATION = "deliberation" + CONFLICT = "conflict" + ACTION_EXEC = "action_exec" + SCORE = "score" + SSE_EVENT = "sse_event" + ERROR = "error" + PROVIDER_CALL = "provider_call" + + +class Event(BaseModel): + """A single benchmark event.""" + + model_config = ConfigDict(frozen=True) + + timestamp: float + tick: int + event_type: EventType + agent: str | None = None + data: dict[str, Any] + + +class RunSummary(BaseModel): + """Aggregate statistics from an EventLog — the CI artifact.""" + + model_config = ConfigDict(frozen=True) + + total_events: int + errors_by_type: dict[str, int] + avg_deliberation_ms: float + action_success_rate: float + total_tokens: int + estimated_cost_usd: float + + +class EventLog: + """Append-only JSONL event logger for benchmark runs.""" + + def __init__(self, path: Path) -> None: + self._path = path + self._path.parent.mkdir(parents=True, exist_ok=True) + self._events: list[Event] = [] + self._file = open(path, "a", encoding="utf-8") # noqa: SIM115 + + def emit( + self, + event_type: EventType, + tick: int, + agent: str | None = None, + **data: Any, + ) -> None: + """Record an event. Writes immediately to JSONL file.""" + event = Event( + timestamp=time.time(), + tick=tick, + event_type=event_type, + agent=agent, + data=data, + ) + self._events.append(event) + self._file.write(event.model_dump_json() + "\n") + self._file.flush() + + def summary(self) -> RunSummary: + """Compute aggregate statistics from recorded events.""" + error_counter: Counter[str] = Counter() + deliberation_latencies: list[float] = [] + action_successes = 0 + action_total = 0 + total_tokens = 0 + total_cost = 0.0 + + for event in self._events: + if event.event_type == EventType.ERROR: + error_counter[str(event.data.get("error_type", "unknown"))] += 1 + elif event.event_type == EventType.DELIBERATION: + latency = event.data.get("latency_ms") + if isinstance(latency, (int, float)): + deliberation_latencies.append(float(latency)) + elif event.event_type == EventType.ACTION_EXEC: + action_total += 1 + if event.data.get("success"): + action_successes += 1 + elif event.event_type == EventType.PROVIDER_CALL: + pt = event.data.get("prompt_tokens", 0) + ct = event.data.get("completion_tokens", 0) + if isinstance(pt, int) and isinstance(ct, int): + total_tokens += pt + ct + cost = event.data.get("estimated_cost", 0.0) + if isinstance(cost, (int, float)): + total_cost += float(cost) + + avg_latency = ( + sum(deliberation_latencies) / len(deliberation_latencies) + if deliberation_latencies + else 0.0 + ) + success_rate = ( + action_successes / action_total if action_total > 0 else 1.0 + ) + + return RunSummary( + total_events=len(self._events), + errors_by_type=dict(error_counter), + avg_deliberation_ms=round(avg_latency, 2), + action_success_rate=round(success_rate, 4), + total_tokens=total_tokens, + estimated_cost_usd=round(total_cost, 6), + ) + + def close(self) -> None: + """Flush and close the log file.""" + if not self._file.closed: + self._file.flush() + self._file.close() + + @property + def events(self) -> list[Event]: + return list(self._events) + + @property + def path(self) -> Path: + return self._path + + def __enter__(self) -> EventLog: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object, + ) -> None: + self.close() diff --git a/src/rle/tracking/history.py b/src/rle/tracking/history.py index a98392b..c164948 100644 --- a/src/rle/tracking/history.py +++ b/src/rle/tracking/history.py @@ -6,6 +6,7 @@ import re from datetime import datetime from pathlib import Path +from typing import Any RESULTS_DIR = Path("results") HISTORY_PATH = RESULTS_DIR / "benchmark_history.jsonl" @@ -21,7 +22,7 @@ def get_run_dir(model: str | None = None) -> Path: return run_dir -def append_history(summary: dict) -> Path: +def append_history(summary: dict[str, object]) -> Path: """Append a benchmark summary as one JSONL line. Returns the history path.""" HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True) with open(HISTORY_PATH, "a") as f: @@ -29,7 +30,7 @@ def append_history(summary: dict) -> Path: return HISTORY_PATH -def load_history() -> list[dict]: +def load_history() -> list[dict[str, object]]: """Load all historical benchmark runs from JSONL.""" if not HISTORY_PATH.exists(): return [] @@ -41,7 +42,7 @@ def load_history() -> list[dict]: return runs -def update_baseline(summary: dict) -> tuple[bool, float | None]: +def update_baseline(summary: dict[str, Any]) -> tuple[bool, float | None]: """Update baseline if this run's avg score is a new best for the model. Returns (is_new_best, previous_score_or_None). diff --git a/src/rle/tracking/leaderboard.py b/src/rle/tracking/leaderboard.py new file mode 100644 index 0000000..dc7f8c7 --- /dev/null +++ b/src/rle/tracking/leaderboard.py @@ -0,0 +1,181 @@ +"""Leaderboard generation from RLE benchmark history. + +Builds model x scenario results matrix with significance markers, +cost-normalized rankings, and Pareto frontier computation. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from rle.scoring.bootstrap import bootstrap_ci + + +class LeaderboardEntry(BaseModel): + """One model's benchmark results.""" + + model_config = ConfigDict(frozen=True) + + model: str + composite_score: float + composite_ci: tuple[float, float] | None = None + total_cost_usd: float = 0.0 + cost_per_scenario: float = 0.0 + total_tokens: int = 0 + total_wall_time_s: float = 0.0 + n_runs: int = 1 + scenarios: dict[str, float] = {} + significance_vs_baseline: dict[str, str] = {} + timestamp: str = "" + git_commit: str = "" + + +def _collect_scenario_scores( + runs: list[dict[str, Any]], +) -> dict[str, list[float]]: + """Group scenario scores across multiple runs for one model.""" + scores: dict[str, list[float]] = {} + for run in runs: + for sc in run.get("scenarios", []): + name = sc.get("name", "") + score = sc.get("score") + if name and isinstance(score, (int, float)): + scores.setdefault(name, []).append(float(score)) + return scores + + +def _per_run_composites(runs: list[dict[str, Any]]) -> list[float]: + """Extract per-run average composite scores.""" + composites: list[float] = [] + for run in runs: + scenario_scores = [ + sc["score"] + for sc in run.get("scenarios", []) + if isinstance(sc.get("score"), (int, float)) + ] + if scenario_scores: + composites.append(sum(scenario_scores) / len(scenario_scores)) + return composites + + +class Leaderboard: + """Manages the RLE benchmark leaderboard.""" + + def from_history(self, history: list[dict[str, Any]]) -> list[LeaderboardEntry]: + """Build sorted leaderboard from benchmark_history.jsonl entries.""" + by_model: dict[str, list[dict[str, Any]]] = {} + for entry in history: + model = entry.get("model", "unknown") + by_model.setdefault(model, []).append(entry) + + entries: list[LeaderboardEntry] = [] + for model, runs in by_model.items(): + latest = runs[-1] + scenario_scores = _collect_scenario_scores(runs) + scenario_means = {k: sum(v) / len(v) for k, v in scenario_scores.items()} + + composites = _per_run_composites(runs) + composite = sum(composites) / len(composites) if composites else 0.0 + n_runs = len(runs) + + ci: tuple[float, float] | None = None + if len(composites) >= 2: + bci = bootstrap_ci(composites) + ci = (bci.ci_lower, bci.ci_upper) + + cost = float(latest.get("cost", {}).get("estimated_cost_usd", 0.0)) + tokens = int(latest.get("cost", {}).get("total_tokens", 0)) + wall = float(latest.get("cost", {}).get("wall_time_s", 0.0)) + n_scenarios = max(len(scenario_means), 1) + + entries.append(LeaderboardEntry( + model=model, + composite_score=round(composite, 4), + composite_ci=ci, + total_cost_usd=cost, + cost_per_scenario=round(cost / n_scenarios, 4), + total_tokens=tokens, + total_wall_time_s=wall, + n_runs=n_runs, + scenarios=scenario_means, + timestamp=str(latest.get("timestamp", "")), + git_commit=str(latest.get("git_commit", "")), + )) + + entries.sort(key=lambda e: e.composite_score, reverse=True) + return entries + + def to_markdown(self, entries: list[LeaderboardEntry]) -> str: + """Render model x scenario matrix as Markdown table.""" + if not entries: + return "" + + all_scenarios = sorted( + {s for e in entries for s in e.scenarios} + ) + short_names = [s.split()[0] if " " in s else s[:12] for s in all_scenarios] + + header = "| Model | " + " | ".join(short_names) + " | Avg | Cost |" + sep = "|" + "|".join("---" for _ in range(len(short_names) + 3)) + "|" + + rows = [header, sep] + for entry in entries: + cells = [entry.model] + for scenario in all_scenarios: + score = entry.scenarios.get(scenario) + sig = entry.significance_vs_baseline.get(scenario, "") + cells.append(f"{score:.2f}{sig}" if score is not None else "—") + cells.append(f"{entry.composite_score:.2f}") + cells.append(f"${entry.total_cost_usd:.2f}") + rows.append("| " + " | ".join(cells) + " |") + + return "\n".join(rows) + + def to_csv(self, entries: list[LeaderboardEntry], path: str) -> None: + """Export leaderboard as CSV.""" + if not entries: + return + + all_scenarios = sorted({s for e in entries for s in e.scenarios}) + header = ["model"] + all_scenarios + ["avg", "cost_usd", "n_runs"] + + lines = [",".join(header)] + for entry in entries: + row = [entry.model] + for s in all_scenarios: + score = entry.scenarios.get(s) + row.append(f"{score:.4f}" if score is not None else "") + row.append(f"{entry.composite_score:.4f}") + row.append(f"{entry.total_cost_usd:.4f}") + row.append(str(entry.n_runs)) + lines.append(",".join(row)) + + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + + def pareto_frontier( + self, entries: list[LeaderboardEntry], + ) -> list[LeaderboardEntry]: + """Return entries on the cost-accuracy Pareto frontier. + + An entry is Pareto-optimal if no other entry has both + higher composite_score AND lower total_cost_usd. + """ + frontier: list[LeaderboardEntry] = [] + for entry in entries: + dominated = any( + other.composite_score >= entry.composite_score + and other.total_cost_usd <= entry.total_cost_usd + and ( + other.composite_score > entry.composite_score + or other.total_cost_usd < entry.total_cost_usd + ) + for other in entries + if other is not entry + ) + if not dominated: + frontier.append(entry) + frontier.sort(key=lambda e: e.composite_score, reverse=True) + return frontier diff --git a/src/rle/tracking/metadata.py b/src/rle/tracking/metadata.py index 01cf119..19aff26 100644 --- a/src/rle/tracking/metadata.py +++ b/src/rle/tracking/metadata.py @@ -20,6 +20,8 @@ def collect_metadata() -> dict[str, object]: "felix_sdk_version": _version("felix-agent-sdk"), "platform": sys.platform, "python_version": platform.python_version(), + "docker_mode": False, + "random_seed": None, } diff --git a/tests/conftest.py b/tests/conftest.py index 660b2c0..6954d33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from felix_agent_sdk.core import HelixConfig, HelixGeometry from felix_agent_sdk.providers.base import BaseProvider from felix_agent_sdk.providers.types import CompletionResult + from rle.config import RLEConfig from rle.rimapi.schemas import ( ColonistData, diff --git a/tests/integration/test_game_loop.py b/tests/integration/test_game_loop.py index 29d75cc..4b0c134 100644 --- a/tests/integration/test_game_loop.py +++ b/tests/integration/test_game_loop.py @@ -11,6 +11,7 @@ from felix_agent_sdk.core import HelixConfig from felix_agent_sdk.providers.base import BaseProvider from felix_agent_sdk.providers.types import CompletionResult + from rle.agents.construction_planner import ConstructionPlanner from rle.agents.defense_commander import DefenseCommander from rle.agents.map_analyst import MapAnalyst diff --git a/tests/integration/test_scenario_run.py b/tests/integration/test_scenario_run.py index e5473eb..e32bb92 100644 --- a/tests/integration/test_scenario_run.py +++ b/tests/integration/test_scenario_run.py @@ -11,6 +11,7 @@ from felix_agent_sdk.core import HelixConfig from felix_agent_sdk.providers.base import BaseProvider from felix_agent_sdk.providers.types import CompletionResult + from rle.agents.construction_planner import ConstructionPlanner from rle.agents.defense_commander import DefenseCommander from rle.agents.medical_officer import MedicalOfficer diff --git a/tests/unit/test_action_resolver.py b/tests/unit/test_action_resolver.py index 1c50355..644737c 100644 --- a/tests/unit/test_action_resolver.py +++ b/tests/unit/test_action_resolver.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest + from rle.agents.actions import Action, ActionPlan from rle.orchestration.action_resolver import ( ActionResolver, diff --git a/tests/unit/test_base_role.py b/tests/unit/test_base_role.py index 1a0e919..f028d7c 100644 --- a/tests/unit/test_base_role.py +++ b/tests/unit/test_base_role.py @@ -10,6 +10,7 @@ from felix_agent_sdk import LLMResult from felix_agent_sdk.core import HelixGeometry from felix_agent_sdk.providers.types import CompletionResult + from rle.agents.actions import ActionPlan, ActionPlanParseError from rle.agents.base_role import RimWorldRoleAgent from rle.rimapi.schemas import GameState diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py new file mode 100644 index 0000000..090ef86 --- /dev/null +++ b/tests/unit/test_bootstrap.py @@ -0,0 +1,147 @@ +"""Tests for bootstrap confidence interval module.""" + +from __future__ import annotations + +import random + +import pytest + +from rle.scoring.bootstrap import BootstrapCI, bootstrap_ci, bootstrap_paired_delta + + +class TestBootstrapCI: + """Tests for bootstrap_ci function.""" + + def test_known_distribution_contains_true_mean(self) -> None: + """CI from uniform [0, 1] samples should contain 0.5.""" + values = [i / 99.0 for i in range(100)] + result = bootstrap_ci(values, n_bootstrap=10_000, seed=42) + assert result.ci_lower <= 0.5 <= result.ci_upper + + def test_ci_width_decreases_with_more_samples(self) -> None: + """Wider CI expected with fewer samples.""" + rng = random.Random(0) + small = [rng.gauss(0.5, 0.1) for _ in range(10)] + rng2 = random.Random(0) + large = [rng2.gauss(0.5, 0.1) for _ in range(100)] + + ci_small = bootstrap_ci(small, n_bootstrap=5_000, seed=1) + ci_large = bootstrap_ci(large, n_bootstrap=5_000, seed=1) + + width_small = ci_small.ci_upper - ci_small.ci_lower + width_large = ci_large.ci_upper - ci_large.ci_lower + assert width_small > width_large + + def test_single_value_returns_degenerate_ci(self) -> None: + """Single value should yield CI where lower == upper == mean.""" + result = bootstrap_ci([0.7]) + assert result.ci_lower == result.mean + assert result.ci_upper == result.mean + assert result.mean == pytest.approx(0.7) + assert result.std == pytest.approx(0.0) + + def test_two_identical_values_ci_lower_equals_upper(self) -> None: + """Two identical values produce CI where lower == upper.""" + result = bootstrap_ci([0.5, 0.5], n_bootstrap=1_000, seed=0) + assert result.ci_lower == pytest.approx(0.5) + assert result.ci_upper == pytest.approx(0.5) + + def test_seed_reproducibility(self) -> None: + """Same seed must produce identical results.""" + values = [0.1, 0.3, 0.5, 0.7, 0.9] + r1 = bootstrap_ci(values, n_bootstrap=1_000, seed=99) + r2 = bootstrap_ci(values, n_bootstrap=1_000, seed=99) + assert r1 == r2 + + def test_different_seeds_produce_same_mean_different_ci(self) -> None: + """Mean is seed-independent; CI bounds vary between seeds.""" + values = [0.1, 0.3, 0.5, 0.7, 0.9, 0.2, 0.4, 0.6, 0.8, 1.0] + r1 = bootstrap_ci(values, n_bootstrap=500, seed=1) + r2 = bootstrap_ci(values, n_bootstrap=500, seed=2) + assert r1.mean == pytest.approx(r2.mean) + assert r1.ci_lower != r2.ci_lower or r1.ci_upper != r2.ci_upper + + def test_empty_values_raises_value_error(self) -> None: + """Empty values list should raise ValueError.""" + with pytest.raises(ValueError, match="empty"): + bootstrap_ci([]) + + def test_n_samples_field(self) -> None: + """n_samples should reflect the input length.""" + values = [0.1, 0.2, 0.3, 0.4, 0.5] + result = bootstrap_ci(values, n_bootstrap=100, seed=0) + assert result.n_samples == 5 + + def test_n_bootstrap_field(self) -> None: + """n_bootstrap should be stored on result.""" + values = [0.1, 0.5, 0.9] + result = bootstrap_ci(values, n_bootstrap=500, seed=0) + assert result.n_bootstrap == 500 + + def test_mean_is_arithmetic_mean(self) -> None: + """Mean field must equal arithmetic mean of inputs.""" + values = [1.0, 2.0, 3.0, 4.0, 5.0] + result = bootstrap_ci(values, n_bootstrap=100, seed=0) + assert result.mean == pytest.approx(3.0) + + def test_ci_bounds_ordered(self) -> None: + """Lower bound must be <= upper bound.""" + values = [0.2, 0.8, 0.4, 0.6, 0.3, 0.7] + result = bootstrap_ci(values, n_bootstrap=1_000, seed=42) + assert result.ci_lower <= result.ci_upper + + def test_result_is_frozen(self) -> None: + """BootstrapCI must be immutable (frozen Pydantic model).""" + result = bootstrap_ci([0.5, 0.6], n_bootstrap=100, seed=0) + with pytest.raises(Exception): + result.mean = 0.0 # type: ignore[misc] + + +class TestBootstrapPairedDelta: + """Tests for bootstrap_paired_delta function.""" + + def test_positive_delta_ci_contains_true_delta(self) -> None: + """When agent consistently outperforms baseline, CI should contain true delta.""" + rng = random.Random(42) + agent = [0.7 + rng.gauss(0.0, 0.05) for _ in range(30)] + rng2 = random.Random(99) + baseline = [0.5 + rng2.gauss(0.0, 0.05) for _ in range(30)] + result = bootstrap_paired_delta(agent, baseline, n_bootstrap=5_000, seed=0) + assert result.ci_lower <= result.mean <= result.ci_upper + assert result.mean == pytest.approx(0.2, abs=0.05) + + def test_negative_delta_ci_is_negative(self) -> None: + """When baseline outperforms agent, CI should be negative.""" + agent = [0.3] * 20 + baseline = [0.7] * 20 + result = bootstrap_paired_delta(agent, baseline, n_bootstrap=1_000, seed=0) + assert result.ci_upper < 0.0 + assert result.mean == pytest.approx(-0.4) + + def test_unequal_lengths_uses_min_length(self) -> None: + """Mismatched list lengths should use min length without error.""" + agent = [0.6, 0.7, 0.8] + baseline = [0.5, 0.5] + result = bootstrap_paired_delta(agent, baseline, n_bootstrap=100, seed=0) + assert result.n_samples == 2 + + def test_identical_scores_delta_is_zero(self) -> None: + """Zero delta between identical agent and baseline scores.""" + scores = [0.5, 0.6, 0.7, 0.8] + result = bootstrap_paired_delta(scores, scores, n_bootstrap=500, seed=0) + assert result.mean == pytest.approx(0.0) + assert result.ci_lower == pytest.approx(0.0) + assert result.ci_upper == pytest.approx(0.0) + + def test_returns_bootstrap_ci_instance(self) -> None: + """Return type must be BootstrapCI.""" + result = bootstrap_paired_delta([0.6, 0.7], [0.5, 0.5], n_bootstrap=100, seed=0) + assert isinstance(result, BootstrapCI) + + def test_seed_reproducibility(self) -> None: + """Same seed produces same result.""" + agent = [0.6, 0.7, 0.8, 0.75] + baseline = [0.5, 0.5, 0.6, 0.55] + r1 = bootstrap_paired_delta(agent, baseline, n_bootstrap=500, seed=7) + r2 = bootstrap_paired_delta(agent, baseline, n_bootstrap=500, seed=7) + assert r1 == r2 diff --git a/tests/unit/test_composite_scorer.py b/tests/unit/test_composite_scorer.py index ca578cb..f38bd23 100644 --- a/tests/unit/test_composite_scorer.py +++ b/tests/unit/test_composite_scorer.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest + from rle.rimapi.schemas import ( ColonyData, GameState, @@ -47,8 +48,8 @@ class TestDefaultWeights: def test_sum_to_one(self) -> None: assert sum(DEFAULT_WEIGHTS.values()) == pytest.approx(1.0) - def test_eight_metrics(self) -> None: - assert len(DEFAULT_WEIGHTS) == 8 + def test_ten_metrics(self) -> None: + assert len(DEFAULT_WEIGHTS) == 10 class TestCompositeScorer: @@ -58,7 +59,7 @@ def test_score_returns_snapshot(self) -> None: assert isinstance(snap, ScoreSnapshot) assert snap.tick == 600000 assert snap.day == 10 - assert len(snap.metrics) == 8 + assert len(snap.metrics) == 10 assert 0.0 <= snap.composite <= 1.0 def test_custom_weights(self) -> None: @@ -87,14 +88,16 @@ def test_averages_snapshots(self) -> None: tick=1, day=1, metrics={"survival": 1.0, "mood": 0.8, "food_security": 0.6, "wealth": 0.5, "research": 0.0, "threat_response": 1.0, - "self_sufficiency": 0.5, "efficiency": 1.0}, + "self_sufficiency": 0.5, "efficiency": 1.0, + "coordination": 0.9, "communication_efficiency": 0.8}, composite=0.7, ), ScoreSnapshot( tick=2, day=2, metrics={"survival": 0.5, "mood": 0.6, "food_security": 0.4, "wealth": 0.3, "research": 0.5, "threat_response": 0.5, - "self_sufficiency": 0.5, "efficiency": 0.5}, + "self_sufficiency": 0.5, "efficiency": 0.5, + "coordination": 0.7, "communication_efficiency": 0.6}, composite=0.5, ), ] diff --git a/tests/unit/test_cost_tracker.py b/tests/unit/test_cost_tracker.py new file mode 100644 index 0000000..7c6ffb5 --- /dev/null +++ b/tests/unit/test_cost_tracker.py @@ -0,0 +1,231 @@ +"""Tests for the CostTracker module.""" + +from __future__ import annotations + +import json +import time +import unittest.mock as mock + +import httpx +import pytest + +from rle.tracking.cost_tracker import ( + CostSnapshot, + CostTracker, + TokenUsage, + create_cost_tracker, + fetch_pricing, +) + +# ------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------ + + +def _make_openrouter_response(models: list[dict]) -> httpx.Response: + data = json.dumps({"data": models}).encode() + return httpx.Response( + status_code=200, + content=data, + headers={"content-type": "application/json"}, + ) + + +def _make_transport_with_response(response: httpx.Response) -> httpx.MockTransport: + def handler(request: httpx.Request) -> httpx.Response: + return response + + return httpx.MockTransport(handler) + + +def _make_error_transport() -> httpx.MockTransport: + def handler(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("connection refused") + + return httpx.MockTransport(handler) + + +def _mock_async_client(transport: httpx.MockTransport) -> mock.MagicMock: + """Build a mock AsyncClient context manager backed by the given transport.""" + real_client = httpx.AsyncClient(transport=transport) + cm = mock.MagicMock() + cm.__aenter__ = mock.AsyncMock(return_value=real_client) + cm.__aexit__ = mock.AsyncMock(return_value=False) + return cm + + +# ------------------------------------------------------------------ +# TokenUsage tests +# ------------------------------------------------------------------ + + +class TestTokenUsage: + def test_total_tokens(self) -> None: + usage = TokenUsage(prompt_tokens=100, completion_tokens=50) + assert usage.total_tokens == 150 + + def test_frozen(self) -> None: + usage = TokenUsage(prompt_tokens=10, completion_tokens=5) + with pytest.raises(Exception): + usage.prompt_tokens = 99 # type: ignore[misc] + + +# ------------------------------------------------------------------ +# CostTracker.record tests +# ------------------------------------------------------------------ + + +class TestCostTrackerRecord: + def test_record_accumulates_tokens(self) -> None: + tracker = CostTracker("test-model", prompt_price=0.0, completion_price=0.0) + tracker.record(TokenUsage(prompt_tokens=100, completion_tokens=50)) + tracker.record(TokenUsage(prompt_tokens=200, completion_tokens=75)) + + snap = tracker.snapshot() + assert snap.total_prompt_tokens == 300 + assert snap.total_completion_tokens == 125 + assert snap.total_tokens == 425 + assert snap.num_calls == 2 + + def test_record_raw_works_as_convenience(self) -> None: + tracker = CostTracker("test-model") + tracker.record_raw(prompt_tokens=80, completion_tokens=40) + tracker.record_raw(prompt_tokens=120, completion_tokens=60) + + snap = tracker.snapshot() + assert snap.total_prompt_tokens == 200 + assert snap.total_completion_tokens == 100 + assert snap.num_calls == 2 + + def test_record_and_record_raw_combined(self) -> None: + tracker = CostTracker("test-model") + tracker.record(TokenUsage(prompt_tokens=50, completion_tokens=25)) + tracker.record_raw(prompt_tokens=50, completion_tokens=25) + + snap = tracker.snapshot() + assert snap.total_prompt_tokens == 100 + assert snap.total_completion_tokens == 50 + assert snap.num_calls == 2 + + +# ------------------------------------------------------------------ +# CostTracker.snapshot cost calculation tests +# ------------------------------------------------------------------ + + +class TestCostTrackerSnapshot: + def test_snapshot_computes_cost_correctly(self) -> None: + tracker = CostTracker( + "some-model", + prompt_price=0.000005, + completion_price=0.000025, + ) + tracker.record(TokenUsage(prompt_tokens=1000, completion_tokens=500)) + + snap = tracker.snapshot() + # 1000 * 0.000005 + 500 * 0.000025 = 0.005 + 0.0125 = 0.0175 + assert snap.estimated_cost_usd == pytest.approx(0.0175, rel=1e-5) + + def test_snapshot_zero_cost_with_default_prices(self) -> None: + tracker = CostTracker("free-model") + tracker.record(TokenUsage(prompt_tokens=10000, completion_tokens=5000)) + + snap = tracker.snapshot() + assert snap.estimated_cost_usd == 0.0 + + def test_snapshot_is_frozen_pydantic_model(self) -> None: + tracker = CostTracker("model") + snap = tracker.snapshot() + assert isinstance(snap, CostSnapshot) + with pytest.raises(Exception): + snap.num_calls = 99 # type: ignore[misc] + + def test_snapshot_returns_correct_total_tokens(self) -> None: + tracker = CostTracker("model", prompt_price=0.001, completion_price=0.002) + tracker.record_raw(500, 250) + + snap = tracker.snapshot() + assert snap.total_tokens == 750 + + def test_wall_time_increases_over_time(self) -> None: + tracker = CostTracker("model") + snap1 = tracker.snapshot() + time.sleep(0.05) + snap2 = tracker.snapshot() + assert snap2.wall_time_s >= snap1.wall_time_s + + +# ------------------------------------------------------------------ +# fetch_pricing tests +# ------------------------------------------------------------------ + + +class TestFetchPricing: + async def test_returns_correct_prices_when_model_found(self) -> None: + models = [ + { + "id": "nvidia/nemotron-3-super-120b-a12b:free", + "pricing": {"prompt": "0.000005", "completion": "0.000025"}, + } + ] + transport = _make_transport_with_response(_make_openrouter_response(models)) + with mock.patch("httpx.AsyncClient", return_value=_mock_async_client(transport)): + result = await fetch_pricing("nvidia/nemotron-3-super-120b-a12b:free") + + assert result == (0.000005, 0.000025) + + async def test_falls_back_on_connection_error(self) -> None: + transport = _make_error_transport() + with mock.patch("httpx.AsyncClient", return_value=_mock_async_client(transport)): + result = await fetch_pricing("some-model") + + assert result == (0.0, 0.0) + + async def test_falls_back_when_model_not_found(self) -> None: + models = [{"id": "different/model", "pricing": {"prompt": "0.001", "completion": "0.002"}}] + transport = _make_transport_with_response(_make_openrouter_response(models)) + with mock.patch("httpx.AsyncClient", return_value=_mock_async_client(transport)): + result = await fetch_pricing("missing/model") + + assert result == (0.0, 0.0) + + async def test_falls_back_on_http_error(self) -> None: + def error_handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(status_code=500, content=b"Internal Server Error") + + transport = httpx.MockTransport(error_handler) + with mock.patch("httpx.AsyncClient", return_value=_mock_async_client(transport)): + result = await fetch_pricing("any/model") + + assert result == (0.0, 0.0) + + +# ------------------------------------------------------------------ +# create_cost_tracker tests +# ------------------------------------------------------------------ + + +class TestCreateCostTracker: + async def test_wires_pricing_into_tracker(self) -> None: + with mock.patch( + "rle.tracking.cost_tracker.fetch_pricing", + new=mock.AsyncMock(return_value=(0.000003, 0.000015)), + ): + tracker = await create_cost_tracker("test/model") + + assert isinstance(tracker, CostTracker) + tracker.record_raw(1000, 500) + snap = tracker.snapshot() + # 1000 * 0.000003 + 500 * 0.000015 = 0.003 + 0.0075 = 0.0105 + assert snap.estimated_cost_usd == pytest.approx(0.0105, rel=1e-5) + + async def test_create_cost_tracker_uses_zero_on_fetch_failure(self) -> None: + with mock.patch( + "rle.tracking.cost_tracker.fetch_pricing", + new=mock.AsyncMock(return_value=(0.0, 0.0)), + ): + tracker = await create_cost_tracker("unknown/model") + + tracker.record_raw(5000, 2000) + snap = tracker.snapshot() + assert snap.estimated_cost_usd == 0.0 diff --git a/tests/unit/test_leaderboard.py b/tests/unit/test_leaderboard.py new file mode 100644 index 0000000..ee30a02 --- /dev/null +++ b/tests/unit/test_leaderboard.py @@ -0,0 +1,132 @@ +"""Tests for the leaderboard generator.""" + +from __future__ import annotations + +from rle.tracking.leaderboard import Leaderboard, LeaderboardEntry + + +def _history_entry( + model: str, + scenarios: list[dict[str, object]], + cost_usd: float = 0.0, + tokens: int = 0, +) -> dict[str, object]: + return { + "model": model, + "scenarios": scenarios, + "cost": {"estimated_cost_usd": cost_usd, "total_tokens": tokens, "wall_time_s": 10.0}, + "timestamp": "2026-04-09T12:00:00Z", + "git_commit": "abc1234", + } + + +def _scenario(name: str, score: float) -> dict[str, object]: + return {"name": name, "score": score} + + +HISTORY = [ + _history_entry("claude-3.5", [_scenario("Crashlanded", 0.82), _scenario("Winter", 0.71)], + cost_usd=2.40, tokens=50000), + _history_entry("gpt-4o", [_scenario("Crashlanded", 0.78), _scenario("Winter", 0.68)], + cost_usd=1.80, tokens=40000), + _history_entry("nemotron-120b", [_scenario("Crashlanded", 0.75), _scenario("Winter", 0.65)], + cost_usd=0.0, tokens=30000), +] + + +class TestFromHistory: + def test_groups_by_model(self) -> None: + lb = Leaderboard() + entries = lb.from_history(HISTORY) + models = [e.model for e in entries] + assert "claude-3.5" in models + assert "gpt-4o" in models + assert "nemotron-120b" in models + + def test_sorted_by_composite_descending(self) -> None: + lb = Leaderboard() + entries = lb.from_history(HISTORY) + scores = [e.composite_score for e in entries] + assert scores == sorted(scores, reverse=True) + + def test_takes_latest_run_per_model(self) -> None: + history = [ + _history_entry("model-a", [_scenario("S1", 0.5)]), + _history_entry("model-a", [_scenario("S1", 0.9)]), + ] + lb = Leaderboard() + entries = lb.from_history(history) + assert len(entries) == 1 + # Composite should reflect both runs averaged + assert entries[0].n_runs == 2 + + def test_empty_history(self) -> None: + lb = Leaderboard() + assert lb.from_history([]) == [] + + +class TestToMarkdown: + def test_produces_valid_markdown(self) -> None: + lb = Leaderboard() + entries = lb.from_history(HISTORY) + md = lb.to_markdown(entries) + lines = md.strip().split("\n") + assert len(lines) >= 3 # header + separator + at least 1 row + assert "| Model |" in lines[0] + assert "---" in lines[1] + + def test_includes_cost_column(self) -> None: + lb = Leaderboard() + entries = lb.from_history(HISTORY) + md = lb.to_markdown(entries) + assert "Cost" in md + assert "$2.40" in md + + def test_includes_significance_markers(self) -> None: + entry = LeaderboardEntry( + model="test", + composite_score=0.8, + scenarios={"S1": 0.82}, + significance_vs_baseline={"S1": "**"}, + ) + lb = Leaderboard() + md = lb.to_markdown([entry]) + assert "0.82**" in md + + def test_empty_entries(self) -> None: + lb = Leaderboard() + assert lb.to_markdown([]) == "" + + +class TestParetoFrontier: + def test_identifies_dominated_entries(self) -> None: + entries = [ + LeaderboardEntry(model="A", composite_score=0.9, total_cost_usd=3.0), + LeaderboardEntry(model="B", composite_score=0.8, total_cost_usd=1.0), + LeaderboardEntry(model="C", composite_score=0.7, total_cost_usd=2.0), + ] + lb = Leaderboard() + frontier = lb.pareto_frontier(entries) + models = [e.model for e in frontier] + assert "A" in models # highest score + assert "B" in models # cheapest with decent score + assert "C" not in models # dominated by B (lower score AND higher cost) + + def test_same_cost_returns_highest_score(self) -> None: + entries = [ + LeaderboardEntry(model="A", composite_score=0.9, total_cost_usd=1.0), + LeaderboardEntry(model="B", composite_score=0.7, total_cost_usd=1.0), + ] + lb = Leaderboard() + frontier = lb.pareto_frontier(entries) + assert len(frontier) == 1 + assert frontier[0].model == "A" + + def test_single_entry(self) -> None: + entry = LeaderboardEntry(model="solo", composite_score=0.5, total_cost_usd=1.0) + lb = Leaderboard() + assert lb.pareto_frontier([entry]) == [entry] + + def test_empty(self) -> None: + lb = Leaderboard() + assert lb.pareto_frontier([]) == [] diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py index b220b1d..b893a87 100644 --- a/tests/unit/test_metrics.py +++ b/tests/unit/test_metrics.py @@ -20,6 +20,8 @@ ) from rle.scoring.metrics import ( MetricContext, + communication_efficiency, + coordination, efficiency, food_security, mood, @@ -226,3 +228,51 @@ def test_no_ticks(self) -> None: def test_empty_plan(self) -> None: ctx = _ctx(tick_results=[_tick_result(0, 0)]) assert efficiency(_state(), ctx) == pytest.approx(1.0) + + +class TestCoordination: + def test_no_conflicts(self) -> None: + ctx = _ctx() + assert coordination(_state(), ctx) == pytest.approx(1.0) + + def test_all_resolved(self) -> None: + ctx = _ctx() + ctx.conflicts_total = 10 + ctx.conflicts_resolved = 10 + assert coordination(_state(), ctx) == pytest.approx(1.0) + + def test_half_resolved(self) -> None: + ctx = _ctx() + ctx.conflicts_total = 8 + ctx.conflicts_resolved = 4 + assert coordination(_state(), ctx) == pytest.approx(0.5) + + def test_none_resolved(self) -> None: + ctx = _ctx() + ctx.conflicts_total = 5 + ctx.conflicts_resolved = 0 + assert coordination(_state(), ctx) == pytest.approx(0.0) + + +class TestCommunicationEfficiency: + def test_no_messages(self) -> None: + ctx = _ctx() + assert communication_efficiency(_state(), ctx) == pytest.approx(1.0) + + def test_all_acted_on(self) -> None: + ctx = _ctx() + ctx.messages_sent = 12 + ctx.messages_acted_on = 12 + assert communication_efficiency(_state(), ctx) == pytest.approx(1.0) + + def test_half_acted_on(self) -> None: + ctx = _ctx() + ctx.messages_sent = 10 + ctx.messages_acted_on = 5 + assert communication_efficiency(_state(), ctx) == pytest.approx(0.5) + + def test_none_acted_on(self) -> None: + ctx = _ctx() + ctx.messages_sent = 7 + ctx.messages_acted_on = 0 + assert communication_efficiency(_state(), ctx) == pytest.approx(0.0) diff --git a/tests/unit/test_rimapi_client.py b/tests/unit/test_rimapi_client.py index dc8e6d8..cf8c439 100644 --- a/tests/unit/test_rimapi_client.py +++ b/tests/unit/test_rimapi_client.py @@ -7,6 +7,7 @@ import httpx import pytest + from rle.rimapi.client import ( RimAPIClient, RimAPIConnectionError, diff --git a/tests/unit/test_role_agents.py b/tests/unit/test_role_agents.py index b907877..710015f 100644 --- a/tests/unit/test_role_agents.py +++ b/tests/unit/test_role_agents.py @@ -6,6 +6,7 @@ from felix_agent_sdk import AgentFactory from felix_agent_sdk.core import HelixConfig, HelixGeometry + from rle.agents import register_rle_agents from rle.agents.actions import ActionPlan from rle.agents.base_role import _SHARED_SYSTEM_PREFIX diff --git a/tests/unit/test_scenario_loader.py b/tests/unit/test_scenario_loader.py index bb676a9..e0ceead 100644 --- a/tests/unit/test_scenario_loader.py +++ b/tests/unit/test_scenario_loader.py @@ -5,6 +5,7 @@ from pathlib import Path import pytest + from rle.scenarios.loader import list_scenarios, load_scenario from rle.scenarios.schema import ScenarioConfig diff --git a/tests/unit/test_schemas.py b/tests/unit/test_schemas.py index 5b187da..579faab 100644 --- a/tests/unit/test_schemas.py +++ b/tests/unit/test_schemas.py @@ -4,6 +4,7 @@ import pytest from pydantic import ValidationError + from rle.rimapi.schemas import ( ColonistData, ColonyData, diff --git a/tests/unit/test_state_manager.py b/tests/unit/test_state_manager.py index 9ca0706..1eefe19 100644 --- a/tests/unit/test_state_manager.py +++ b/tests/unit/test_state_manager.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest + from rle.orchestration.state_manager import GameStateManager from rle.rimapi.schemas import GameState diff --git a/tests/unit/test_visualizer_integration.py b/tests/unit/test_visualizer_integration.py index eabd74e..565b8a2 100644 --- a/tests/unit/test_visualizer_integration.py +++ b/tests/unit/test_visualizer_integration.py @@ -4,6 +4,7 @@ from felix_agent_sdk.core import HelixConfig from felix_agent_sdk.visualization import HelixVisualizer + from rle.agents import AGENT_DISPLAY diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6af1f09 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1183 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/2d/fc5c5a369db977efbaa646d77ba42b38a6de4e95789884032b0e2e3fc834/anthropic-0.92.0.tar.gz", hash = "sha256:d1e792ed0692379452a1af6b266df495e973c3695cd0aace2a108b838393cbc4", size = 652420, upload-time = "2026-04-08T16:55:35.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/21/bf5b5ab10b6932c5c43eaa66b6e3f256de569cf0323d89f9cc281a0d0f39/anthropic-0.92.0-py3-none-any.whl", hash = "sha256:f92a4bd065d5cab90a96b65bb44e473bf7c6fe731a743cd156e9ad1d245c381e", size = 621195, upload-time = "2026-04-08T16:55:33.639Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "felix-agent-sdk" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/bb/d39753d61b6f6e90106762c2f3b60ce001c3de5bb4de80d84147667a14c6/felix_agent_sdk-0.2.1.tar.gz", hash = "sha256:8788d983028bde4248abd80562ce72a2f536f935e718a150023b801714eb6241", size = 163584, upload-time = "2026-03-21T20:48:55.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/69/f63d280921ac2075d523b5dc6915e4607a9701cbea6c3b01707c09e9e5a9/felix_agent_sdk-0.2.1-py3-none-any.whl", hash = "sha256:9a95e987aa46c73328f1e0f55a0bba56f81e29ea2ea49ffce6cd43c05f712d3c", size = 117084, upload-time = "2026-03-21T20:48:53.359Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "anthropic" }, + { name = "openai" }, +] +anthropic = [ + { name = "anthropic" }, +] +local = [ + { name = "openai" }, +] +openai = [ + { name = "openai" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/65/fb800d327bf25bf31b798dd08935d326d064ecb9b359059fecd91b3a98e8/huggingface_hub-1.9.2.tar.gz", hash = "sha256:8d09d080a186bd950a361bfc04b862dfb04d6a2b41d48e9ba1b37507cfd3f1e1", size = 750284, upload-time = "2026-04-08T08:43:11.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/d4/e33bf0b362810a9b96c5923e38908950d58ecb512db42e3730320c7f4a3a/huggingface_hub-1.9.2-py3-none-any.whl", hash = "sha256:e1e62ce237d4fbeca9f970aeb15176fbd503e04c25577bfd22f44aa7aa2b5243", size = 637349, upload-time = "2026-04-08T08:43:09.114Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, + { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "openai" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/fe/64b3d035780b3188f86c4f6f1bc202e7bb74757ef028802112273b9dcacf/openai-2.31.0.tar.gz", hash = "sha256:43ca59a88fc973ad1848d86b98d7fac207e265ebbd1828b5e4bdfc85f79427a5", size = 684772, upload-time = "2026-04-08T21:01:41.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rimworld-learning-environment" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "felix-agent-sdk" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, +] + +[package.optional-dependencies] +all = [ + { name = "felix-agent-sdk", extra = ["all"] }, +] +anthropic = [ + { name = "felix-agent-sdk", extra = ["anthropic"] }, +] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-pyyaml" }, +] +local = [ + { name = "felix-agent-sdk", extra = ["local"] }, +] +openai = [ + { name = "felix-agent-sdk", extra = ["openai"] }, +] +tracking = [ + { name = "huggingface-hub" }, + { name = "wandb" }, +] +viz = [ + { name = "matplotlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "felix-agent-sdk", specifier = ">=0.2.1" }, + { name = "felix-agent-sdk", extras = ["all"], marker = "extra == 'all'" }, + { name = "felix-agent-sdk", extras = ["anthropic"], marker = "extra == 'anthropic'" }, + { name = "felix-agent-sdk", extras = ["local"], marker = "extra == 'local'" }, + { name = "felix-agent-sdk", extras = ["openai"], marker = "extra == 'openai'" }, + { name = "httpx", specifier = ">=0.24" }, + { name = "huggingface-hub", marker = "extra == 'tracking'", specifier = ">=0.20" }, + { name = "matplotlib", marker = "extra == 'viz'", specifier = ">=3.5" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pydantic-settings", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "wandb", marker = "extra == 'tracking'", specifier = ">=0.15" }, +] +provides-extras = ["anthropic", "openai", "local", "all", "dev", "viz", "tracking"] + +[[package]] +name = "ruff" +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.57.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wandb" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/bb/eb579bf9abac70934a014a9d4e45346aab307994f3021d201bebe5fa25ec/wandb-0.25.1.tar.gz", hash = "sha256:b2a95cd777ecbe7499599a43158834983448a0048329bc7210ef46ca18d21994", size = 43983308, upload-time = "2026-03-10T23:51:44.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/d8/873553b6818499d1b1de314067d528b892897baf0dc81fedc0e845abc2dd/wandb-0.25.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:9bb0679a3e2dcd96db9d9b6d3e17d046241d8d122974b24facb85cc93309a8c9", size = 23615900, upload-time = "2026-03-10T23:51:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/71/ea/b131f319aaa5d0bf7572b6bfcff3dd89e1cf92b17eee443bbab71d12d74c/wandb-0.25.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:0fb13ed18914027523e7b4fc20380c520e0d10da0ee452f924a13f84509fbe12", size = 25576144, upload-time = "2026-03-10T23:51:11.527Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/81508581f0bb77b0495665c1c78e77606a48e66e855ca71ba7c8ae29efa4/wandb-0.25.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cc4521eb5223429ddab5e8eee9b42fdf4caabdf0bc4e0e809042720e5fbef0ed", size = 23070425, upload-time = "2026-03-10T23:51:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c7/445155ef010e2e35d190797d7c36ff441e062a5b566a6da4778e22233395/wandb-0.25.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e73b4c55b947edae349232d5845204d30fac88e18eb4ad1d4b96bf7cf898405a", size = 25628142, upload-time = "2026-03-10T23:51:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/f5c55ee00cf481ef1ccd3c385a0585ad52e7840d08419d4f82ddbeeea959/wandb-0.25.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:22b84065aa398e1624d2e5ad79e08bc4d2af41a6db61697b03b3aaba332977c6", size = 23123172, upload-time = "2026-03-10T23:51:23.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/19eb7974c0e9253bcbaee655222c0f0e1a52e63e9479ee711b4208f8ac31/wandb-0.25.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:005c4c6b5126ef8f4b4110e5372d950918b00637d6dc4b615ad17445f9739478", size = 25714479, upload-time = "2026-03-10T23:51:27.421Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/466c1d03323a4a0ed7d4036a59b18d6b6f67cb5032e444205927e226b18d/wandb-0.25.1-py3-none-win32.whl", hash = "sha256:8f2d04f16b88d65bfba9d79fb945f6c64e2686215469a841936e0972be8ec6a5", size = 24967338, upload-time = "2026-03-10T23:51:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/89/22/680d34c1587f3a979c701b66d71aa7c42b4ef2fdf0774f67034e618e834e/wandb-0.25.1-py3-none-win_amd64.whl", hash = "sha256:62db5166de14456156d7a85953a58733a631228e6d4248a753605f75f75fb845", size = 24967343, upload-time = "2026-03-10T23:51:36.026Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e8/76836b75d401ff5912aaf513176e64557ceaec4c4946bfd38a698ff84d48/wandb-0.25.1-py3-none-win_arm64.whl", hash = "sha256:cc7c34b70cf4b7be4d395541e82e325fd9d2be978d62c9ec01f1a7141523b6bb", size = 22080774, upload-time = "2026-03-10T23:51:40.196Z" }, +]