diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2420a3e..13317ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,9 +77,8 @@ jobs: matrix: module: - hook-context-intelligence - - tool-blob-read + - tool-context-intelligence-query - tool-context-intelligence-upload - - tool-graph-query steps: - uses: actions/checkout@v4 @@ -90,10 +89,18 @@ jobs: - name: Install dependencies working-directory: modules/${{ matrix.module }} # --frozen: respect the module's own lockfile exactly as committed. - # The module's path dependency on `../..` is resolved from the - # checked-out root, so no special setup is needed. + # The module declares the shared bundle as a @main git ref (NOT a path + # source), so uv installs the *published* context_intelligence. The Run + # tests step shadows it with the in-repo source (below). run: uv sync --frozen - name: Run tests working-directory: modules/${{ matrix.module }} + # Test the module against the IN-REPO shared library, which may carry + # not-yet-published changes (e.g. context_intelligence/tool_resolver.py) + # that @main only gains when this PR merges. Put the checked-out root on + # PYTHONPATH to shadow the @main-installed copy — the same convention + # AGENTS.md prescribes for local runs. + env: + PYTHONPATH: ${{ github.workspace }} run: uv run pytest tests/ -q --tb=short --ignore=tests/dtu diff --git a/README.md b/README.md index 5c46fc7..b0926fb 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ overrides: include: ["**"] # every session (the default) ``` -That single-destination block is the common case. To route different projects to different servers — or to exclude some entirely — add more named destinations; see [Multi-server fan-out](#multi-server-fan-out-destinations). For the full routing model and pattern rules, see the [Configuration reference](#configuration-reference). +That single-destination block is the common case. To route different projects to different servers — or to exclude some entirely — add more named destinations; for the full routing model and pattern rules, see [Server forwarding — `destinations`](#server-forwarding--destinations) in the Configuration reference. > **Never write a literal API key into `settings.yaml`.** That file is version-controllable configuration; a secret written there is one accidental commit away from exposure. Always reference it via `${VAR}` from `keys.env`. @@ -134,78 +134,7 @@ When this bundle is loaded through the [Amplifier app-cli](https://github.com/mi `~/.amplifier/settings.yaml` is the app-cli's knob for bundle configuration. It is safe to commit to version control **as long as secrets are referenced via `${VAR_NAME}` interpolation**, never as literal values. The actual secrets stay exclusively in `~/.amplifier/keys.env`. The `${...}` placeholder is resolved by the app-cli **before** the value reaches the hook — the hook reads only its mount config dict and never the environment directly, so the variable name in `keys.env` is entirely your choice (it never has to match any `AMPLIFIER_*` convention). -### Configuring servers with `destinations` (current shape) - -`destinations` is a named map of servers under `overrides.hook-context-intelligence.config`. Each entry has a `url`, an `api_key`, and optional `.gitignore`-style `include`/`exclude` patterns that decide which sessions route to it (by working directory). This is the canonical configuration shape: - -```bash -# ~/.amplifier/keys.env — secrets only, never commit this file -PERSONAL_CI_KEY= -TEAM_CI_KEY= -``` - -```yaml -# ~/.amplifier/settings.yaml -overrides: - hook-context-intelligence: - config: - workspace: "my-project" # optional — auto-resolved if omitted - destinations: - personal: - url: "http://localhost:8000" - api_key: "${PERSONAL_CI_KEY}" - include: ["**"] # all sessions... - exclude: ["**/client-*/"] # ...except any client-* project dir and below - team: - url: "https://ci.team.example" - api_key: "${TEAM_CI_KEY}" - include: ["**/work/"] # only sessions under a "work" directory -``` - -A session's events are sent to **every** destination it matches (true fan-out — zero, one, or several), and **local JSONL is always written** regardless. The per-destination `api_key` becomes the `Authorization: Bearer ` header on that server's POSTs; because each destination references its own `${VAR}`, distinct keys never cross between servers. For the full routing model and pattern semantics see [Multi-server fan-out](#multi-server-fan-out-destinations) and the [Configuration reference](#configuration-reference). - -### Multi-server fan-out (`destinations`) - -The example above already shows two servers. This section documents the full routing model. - -`destinations` is a dict keyed by name, under the `overrides.hook-context-intelligence.config` block: - -```yaml -# ~/.amplifier/settings.yaml -overrides: - hook-context-intelligence: - config: - destinations: - personal: - url: "${PERSONAL_CI_URL}" - api_key: "${PERSONAL_CI_KEY}" # secret lives in keys.env, referenced here - include: ["**"] # all sessions... - exclude: ["**/client-*/"] # ...except any client-* project dir and everything under it - team: - url: "${TEAM_CI_URL}" - api_key: "${TEAM_CI_KEY}" - include: ["**/work/"] # only sessions under a "work" directory -``` - -**How routing is decided.** For each session the hook derives a match key from the session's working directory (the `session.working_dir` capability) and tests it against every destination. A destination is **active** for a session iff the working dir matches an `include` pattern **and** does not match an `exclude` pattern — **exclude wins, per destination**. The session's events are sent to **every** active destination (true fan-out): a session can match zero, one, or several. **Local JSONL is always written**, regardless of how many destinations match. - -**Pattern semantics — `.gitignore` rules.** `include` / `exclude` patterns use `.gitignore` (gitwildmatch) semantics, matched against the session's working **directory**: - -| Pattern | Matches | -|---------|---------| -| `foo/`, `foo`, `**/foo/`, `**/foo` | the directory `foo` **and everything beneath it** | -| `**` | every session | -| empty `include` list | nothing (the destination is inactive) | - -Prefer the trailing-slash directory form (e.g. `**/work/`) to mean "this project and all its sessions" — it matches whether the session is launched from the project **root** or any subdirectory. (A pattern that targets only contents, like `**/work/**`, still also matches the directory itself here, because the match key is a directory.) - -**Defaults & validation.** Omitted `include` defaults to `["**"]` (match everything); omitted `exclude` defaults to none. After `${VAR}` expansion, a `destinations` entry whose `url` **or** `api_key` is empty is a **mount error** (fail-fast, naming the offending destination). With no `destinations` and no legacy url, the hook is local-JSONL-only. - -**Legacy single-server behavior differs on one point.** When only the legacy scalars are set (no `destinations`) and the `url` is present but the `api_key` is empty — e.g. an unset `${AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY:}` — the hook **degrades to local-only and logs a WARNING; it does not fail to mount**. (A `destinations` entry with an empty url/api_key is a hard error; the legacy scalar path is intentionally lenient for back-compat.) - -**Per-project override.** Because `destinations` is keyed by name, a project `.amplifier/settings.yaml` can override a single destination's `include`/`exclude` (e.g. `destinations.team.include`) without restating the others — the app-cli deep-merges user → project settings. - -> **Secrets:** keep `api_key` values in `~/.amplifier/keys.env` and reference them via `${VAR}` in `settings.yaml`. Never write a literal key into `settings.yaml`. +The actual `destinations` configuration — its sub-keys, routing rules, pattern semantics, defaults, validation, and per-project overrides — lives in one place: [Server forwarding — `destinations`](#server-forwarding--destinations) in the Configuration reference. --- @@ -278,10 +207,10 @@ cleanup = await mount(coordinator, config={ ### Accessing resolved values -`mount()` registers a `ConfigResolver` as the `context_intelligence.config_resolver` capability: +`mount()` registers a `HookConfigResolver` as the `context_intelligence.hook_config_resolver` capability: ```python -resolver = coordinator.get_capability("context_intelligence.config_resolver") +resolver = coordinator.get_capability("context_intelligence.hook_config_resolver") resolver.workspace # resolved workspace string resolver.base_path # resolved Path object resolver.session_dir("abc-123") # Path to a session's CI directory @@ -293,18 +222,52 @@ resolver.session_dir("abc-123") # Path to a session's CI directory The `config` dict passed to `mount()` uses the same keys as the `overrides.hook-context-intelligence.config` block in `settings.yaml`. The hook is a **pure mount-config consumer**: it reads only this config dict (plus coordinator capabilities) and does **not** read environment variables or `settings.yaml` itself. Environment variables reach the config only because the shipped behavior YAML (and any `settings.yaml` override) reference them through `${VAR}` / `${VAR:default}` placeholders, which the Amplifier app-cli expands before `mount()` is called. There is **no** automatic `AMPLIFIER_CONTEXT_INTELLIGENCE_` → config-key mapping — an env var with no corresponding `${VAR}` placeholder in the active config never reaches the hook. The **Env var** column below names the variable each shipped placeholder reads. -#### Server forwarding — `destinations` (current shape) +#### Server forwarding — `destinations` -`destinations` is a dict keyed by destination name. Each value is a dict with these keys: +`destinations` is a dict keyed by destination name, under `overrides.hook-context-intelligence.config`. Each value is a dict with these keys: | Sub-key | Required | Default | Description | |---------|----------|---------|-------------| | `url` | yes | — | Base URL of the CI server for this destination. | -| `api_key` | yes | — | Bearer token for this destination. Sent as `Authorization: Bearer ` on that destination's POSTs only. | +| `api_key` | yes | — | Bearer token for this destination. Sent as `Authorization: Bearer ` on that destination's POSTs only — because each destination references its own `${VAR}`, distinct keys never cross between servers. | | `include` | no | `["**"]` | `.gitignore`-style patterns matched against the session's working directory. The destination is a candidate when any pattern matches. | | `exclude` | no | `[]` | `.gitignore`-style patterns; if any matches, the destination is dropped for that session (**exclude wins**). | -Routing: a session is sent to **every** destination where `include` matches **and** `exclude` does not. **Local JSONL is always written**, even when no destination matches. After `${VAR}` expansion, a destination with an empty `url` **or** `api_key` is a **mount error** (fail-fast, naming the offending destination). With no `destinations` configured, the hook is local-JSONL-only. See [Multi-server fan-out](#multi-server-fan-out-destinations) for the full pattern semantics. +```yaml +# ~/.amplifier/settings.yaml — route different projects to different servers +overrides: + hook-context-intelligence: + config: + workspace: "my-project" # optional — auto-resolved if omitted + destinations: + personal: + url: "http://localhost:8000" + api_key: "${PERSONAL_CI_KEY}" # secret lives in keys.env, referenced here + include: ["**"] # all sessions... + exclude: ["**/client-*/"] # ...except any client-* project dir and below + team: + url: "https://ci.team.example" + api_key: "${TEAM_CI_KEY}" + include: ["**/work/"] # only sessions under a "work" directory +``` + +**Routing.** For each session the hook derives a match key from the session's working directory (the `session.working_dir` capability) and tests it against every destination. A destination is **active** for a session iff the working dir matches an `include` pattern **and** does not match an `exclude` pattern — **exclude wins, per destination**. The session's events are sent to **every** active destination (true fan-out): a session can match zero, one, or several. **Local JSONL is always written**, regardless of how many destinations match. + +**Pattern semantics — `.gitignore` rules.** `include` / `exclude` patterns use `.gitignore` (gitwildmatch) semantics, matched against the session's working **directory**: + +| Pattern | Matches | +|---------|---------| +| `foo/`, `foo`, `**/foo/`, `**/foo` | the directory `foo` **and everything beneath it** | +| `**` | every session | +| empty `include` list | nothing (the destination is inactive) | + +Prefer the trailing-slash directory form (e.g. `**/work/`) to mean "this project and all its sessions" — it matches whether the session is launched from the project **root** or any subdirectory. (A pattern that targets only contents, like `**/work/**`, still also matches the directory itself here, because the match key is a directory.) + +**Defaults & validation.** Omitted `include` defaults to `["**"]` (match everything); omitted `exclude` defaults to none. After `${VAR}` expansion, a `destinations` entry whose `url` **or** `api_key` is empty is a **mount error** (fail-fast, naming the offending destination). With no `destinations` configured, the hook is local-JSONL-only. (The legacy scalar path is intentionally more lenient — see [Deprecated — legacy single-server scalars](#deprecated--legacy-single-server-scalars) below.) + +**Per-project override.** Because `destinations` is keyed by name, a project `.amplifier/settings.yaml` can override a single destination's `include`/`exclude` (e.g. `destinations.team.include`) without restating the others — the app-cli deep-merges user → project settings. + +> **Secrets:** keep `api_key` values in `~/.amplifier/keys.env` and reference them via `${VAR}` in `settings.yaml`. Never write a literal key into `settings.yaml`. #### Other config keys @@ -334,6 +297,47 @@ Legacy degradation (differs from `destinations`): if `context_intelligence_serve --- +#### Query tools (`graph-query`, `blob-read`) — read-side endpoint + +The hook config above governs **where events are written** (the upload / fan-out side). The **query tools** `graph_query` and `blob_read` (both mounted by the `tool-context-intelligence-query` module) are the **read side** — they call a Context Intelligence server to answer graph queries and fetch blobs. They share a single `ToolConfigResolver` built once in `mount()`, so **a single config namespace** (`overrides.tool-context-intelligence-query.config`) serves both tools. They resolve their `(server_url, api_key)` independently per field, **explicit-read-config first**, and the chain is designed so that **configuring `destinations` alone is enough** — you do **not** have to repeat the endpoint for queries: + +| Order | Source | Notes | +|-------|--------|-------| +| **1** | First entry of `sources` on the tool's own config (`overrides.tool-context-intelligence-query.config`) | The explicit read override. Wins when set. Applies to both `graph_query` and `blob_read` — configure once. | +| **2** | First entry of the hook's `destinations` block | **The common case** — queries follow the same server you upload to, with zero extra config. This is the bridge that makes a `destinations`-only setup "just work" for reads. | +| **3** | Env `AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL` / `AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY` | Single canonical last-resort fallback (reached via `${VAR}` placeholders in the shipped YAML, same convention as everywhere else). | +| — | else | `configuration_error: "context-intelligence server URL not configured"`. | + +Each field walks the chain independently (a tier that supplies a `url` but no `api_key` lets `api_key` fall through). **Env is a true fallback — it never outranks the hook destination** (tier 2). There are **no** `*_PRIVATE_*` environment variables; the only env names consulted are the canonical `AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL` / `_API_KEY`. + +**`sources`** is a mapping keyed by name, mirroring the hook's `destinations` shape. **Only a single read source is supported in this version** — the read path does **not** fan out to multiple sources. Configure exactly one entry (conventionally named `default`); if more than one is present, only the **first** (declaration / insertion order) is used and the rest are ignored. + +In most setups you configure nothing here: **with no `sources` set, the read tools use the first configured `destination` in the hook config as their read source** (tier 2 below). Reach for `sources` only when the read endpoint must differ from the upload destination (e.g. a read replica or a debugging override) — it overrides the **read path only** and does not change where the hook uploads: + +```yaml +# ~/.amplifier/settings.yaml — only needed when queries must hit a DIFFERENT server than uploads. +# One config namespace covers both graph_query and blob_read tools — configure once. +# Only ONE read source is supported; name it `default`. +overrides: + tool-context-intelligence-query: + config: + sources: + default: # the single read source (only the first entry is honored) + url: "http://read-replica.example.com" + api_key: "${CI_READ_KEY}" # secret lives in keys.env, referenced here +``` + +| Sub-key | Required | Default | Description | +|---------|----------|---------|-------------| +| `url` | yes | — | Base URL of the CI server the query tool reads from. | +| `api_key` | yes | — | Bearer token for read requests to that server. | + +**Legacy back-compat (read side):** with no `sources` key present, explicit top-level scalars on the tool config (`context_intelligence_server_url` + `context_intelligence_api_key`, **both** required) synthesize a single `default` read entry at tier 1 — symmetric to the hook's legacy synthesis. With neither set, resolution falls through to the hook destination (tier 2) and then env (tier 3). + +> **Most users configure nothing here.** A single hook `destinations` entry already powers both upload and query. `sources` exists only for the read-replica / split-endpoint case. + +--- + ## Server dispatch ### Dispatch isolation @@ -483,7 +487,7 @@ amplifier-bundle-context-intelligence/ │ ├── event-schema.md ← all 51+ Amplifier events │ ├── graph-model-reference.md ← Neo4j graph model for Cypher queries │ ├── safe-extraction-patterns.md ← JSONL navigation patterns -│ ├── config-resolution.dot ← ConfigResolver fallback chain diagram +│ ├── config-resolution.dot ← HookConfigResolver fallback chain diagram │ ├── session-disk-layout.dot ← on-disk session directory structure │ ├── delegation-strategy.dot ← graph-analyst → session-navigator delegation logic │ ├── agents/ @@ -492,8 +496,7 @@ amplifier-bundle-context-intelligence/ │ └── jsonl-event-schema.md ← events.jsonl schema contract ├── modules/ │ ├── hook-context-intelligence/ ← the Python hook module -│ ├── tool-graph-query/ ← graph_query tool module -│ └── tool-blob-read/ ← blob_read tool module +│ └── tool-context-intelligence-query/ ← graph_query + blob_read tools ├── docs/ │ ├── context-intelligence-exploration-guide.md ← what to explore and how to test │ ├── dispatch-circuit-breaker.dot ← dispatch flow and circuit breaker state machine diff --git a/agents/graph-analyst.md b/agents/graph-analyst.md index 780cc05..0d3b55c 100644 --- a/agents/graph-analyst.md +++ b/agents/graph-analyst.md @@ -38,10 +38,8 @@ model_role: [reasoning, general] tools: - module: tool-delegate source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/tool-delegate - - module: tool-graph-query - source: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=modules/tool-graph-query - - module: tool-blob-read - source: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=modules/tool-blob-read + - module: tool-context-intelligence-query + source: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=modules/tool-context-intelligence-query - module: tool-filesystem source: git+https://github.com/microsoft/amplifier-module-tool-filesystem@main config: diff --git a/bundle.dot b/bundle.dot index c7d8b5b..f15e890 100644 --- a/bundle.dot +++ b/bundle.dot @@ -1,55 +1,55 @@ +// This repository packages tools and AI assistants that record, search, and analyze Amplifier's own work-session history. digraph context_intelligence { rankdir=LR fontname="Helvetica" fontsize=12 - label="context-intelligence v0.1.0 — bundle repo" + label="Context Intelligence — Session History Toolkit\nA bundle that records and analyzes Amplifier's past work sessions (v0.1.0)" labelloc=t labeljust=c nodesep=0.6 ranksep=0.7 bgcolor="white" - source_hash="dba7ae999a5fd095337bcd9ede4c54b866044c0b31d63d20c4b0ef4483abaf6c" + source_hash="4636e62f1ae10ff3f60e2b22dcb755c49a7851ad51669ebb1fcb900c54a33559" node [fontname="Helvetica", fontsize=11, style="filled,rounded"] edge [fontname="Helvetica", fontsize=9] - root_context_intelligence [label="context-intelligence v0.1.0\n0 tools · 0 agents\n~98 tok aggregate", shape=box, fillcolor="#80cbc4", style="filled,rounded,bold", penwidth=2] + root_context_intelligence [label="Context Intelligence (Main Package)\n0 tools · 0 agents\n~98 tok aggregate", shape=box, fillcolor="#80cbc4", style="filled,rounded,bold", penwidth=2] subgraph cluster_behaviors { - label="Behaviors" + label="Capability Bundles" style="filled" fillcolor="#f9f9f9" color="#999999" - beh_context_intelligence_behavior [label="context-intelligence-behavior\n4 tools\n~1832 tok", shape=box, fillcolor="#e0f2f1", style="filled,rounded"] + beh_context_intelligence_behavior [label="Session Analysis Toolkit\n4 tools\n~2549 tok", shape=box, fillcolor="#e0f2f1", style="filled,rounded"] } subgraph cluster_agents { - label="Agents" + label="Specialist Assistants" style="filled" fillcolor="#f9f9f9" color="#999999" - agt_context_intelligence_design_facilitator [label="context-intelligence-design-facilitator\n~187 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] - agt_context_intelligence_tool_designer [label="context-intelligence-tool-designer\n~198 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] - agt_graph_analyst [label="graph-analyst\n~543 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] - agt_session_navigator [label="session-navigator\n~366 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] + agt_context_intelligence_design_facilitator [label="Design Helper\n(guides new features)\n~187 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] + agt_context_intelligence_tool_designer [label="Tool Builder Helper\n~198 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] + agt_graph_analyst [label="Session Graph Analyst\n(answers questions about sessions)\n~543 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] + agt_session_navigator [label="Session File Navigator\n(reads raw session files)\n~366 tok desc", shape=box, fillcolor="#c8e6c9", style="filled,rounded"] } subgraph cluster_modules { - label="Modules" + label="Building Blocks (Code Components)" style="filled" fillcolor="#f9f9f9" color="#999999" - mod_hook_context_intelligence [label="hook-context-intelligence", shape=box, fillcolor="#bbdefb", style="filled,rounded"] - mod_tool_blob_read [label="tool-blob-read", shape=box, fillcolor="#bbdefb", style="filled,rounded"] - mod_tool_context_intelligence_upload [label="tool-context-intelligence-upload", shape=box, fillcolor="#bbdefb", style="filled,rounded"] - mod_tool_graph_query [label="tool-graph-query", shape=box, fillcolor="#bbdefb", style="filled,rounded"] + mod_hook_context_intelligence [label="Auto Session Recorder", shape=box, fillcolor="#bbdefb", style="filled,rounded"] + mod_tool_context_intelligence_query [label="Session Search Tool", shape=box, fillcolor="#bbdefb", style="filled,rounded"] + mod_tool_context_intelligence_upload [label="Session Upload Tool", shape=box, fillcolor="#bbdefb", style="filled,rounded"] } subgraph cluster_legend { - label="Legend" + label="Legend (Color Key)" style="filled" fillcolor="white" color="#cccccc" @@ -69,7 +69,7 @@ digraph context_intelligence { disclaimer [label="Token estimates: ~4 chars/token\nSolid border = local (counted)\nDashed + red = external, hidden cost (not counted)\nDashed + muted = external, no cost\nExcludes: sub-session costs, runtime-dynamic", shape=note, fillcolor="#eceff1", style="filled", fontsize=9] - ext_githttps___github_com_microsoft_amplifier_foundation_main [label="amplifier-foundation\n(external, cost)", shape=box, fillcolor="#80cbc4", style="dashed", color="red", penwidth=2] + ext_githttps___github_com_microsoft_amplifier_foundation_main [label="amplifier-foundation\n(shared base, external)", shape=box, fillcolor="#80cbc4", style="dashed", color="red", penwidth=2] root_context_intelligence -> ext_githttps___github_com_microsoft_amplifier_foundation_main [style=dashed] root_context_intelligence -> beh_context_intelligence_behavior [label="composes"] diff --git a/bundle.png b/bundle.png index b0b34e1..ee8de52 100644 Binary files a/bundle.png and b/bundle.png differ diff --git a/context/config-resolution.dot b/context/config-resolution.dot index 249df50..8cfbd48 100644 --- a/context/config-resolution.dot +++ b/context/config-resolution.dot @@ -1,19 +1,26 @@ -// ConfigResolver — lazy fallback chain for hook configuration values. +// Config resolution — two resolvers, lazy fallback chains. // -// Each property is resolved on first access and cached. -// Empty strings in config are treated as absent and fall through -// to the next source in the chain (standard or-chain falsy semantics). +// HookConfigResolver (write/upload side) — owns workspace, paths, and the +// `destinations` fan-out map. +// ToolConfigResolver (read/query side) — used by the graph_query and +// blob_read tools; resolves the query endpoint via +// resolve_query_endpoint(), EXPLICIT-READ-CONFIG FIRST, +// then falling back to the first hook destination, then env. +// +// Each value is resolved on first access and cached. Empty strings are treated +// as absent and fall through to the next source (or-chain falsy semantics). // // Render: dot -Tsvg config-resolution.dot -o config-resolution.svg -digraph ConfigResolver { +digraph ConfigResolution { rankdir=TB; fontname="Helvetica"; node [fontname="Helvetica", shape=box, style="rounded,filled", fillcolor="#f0f0f0"]; edge [fontname="Helvetica", fontsize=10]; - // --- Central node --- - resolver [label="ConfigResolver", shape=component, fillcolor="#d4e6f1", style="filled"]; + // --- Central nodes --- + hook_resolver [label="HookConfigResolver\n(hook / write side)", shape=component, fillcolor="#d4e6f1", style="filled"]; + tool_resolver [label="ToolConfigResolver\n(tool-context-intelligence-query\ngraph_query + blob_read — read side)", shape=component, fillcolor="#d1f2d4", style="filled"]; // --- Input sources --- subgraph cluster_inputs { @@ -22,14 +29,16 @@ digraph ConfigResolver { color="#999999"; hook_config [label="hook config\n(behavior YAML)"]; + tool_config [label="tool config\n(overrides.tool-context-intelligence-query.config)"]; coord_config [label="coordinator.config\n(set by app at session creation)"]; session_cap [label="session.working_dir\ncapability\n(set by foundation bundle.py)"]; env_vars [label="Environment Variables\nAMPLIFIER_CONTEXT_INTELLIGENCE_*\n(expanded in YAML before mount)", shape=note, fillcolor="#fff3cd"]; } env_vars -> hook_config [label="expanded into", style=dashed]; + env_vars -> tool_config [label="expanded into", style=dashed]; - // --- project_slug resolution --- + // --- project_slug resolution (hook) --- subgraph cluster_project_slug { label="project_slug resolution"; style=filled; @@ -46,7 +55,7 @@ digraph ConfigResolver { ps_working_dir -> ps_default [label="empty?"]; } - // --- base_path resolution --- + // --- base_path resolution (hook) --- subgraph cluster_base_path { label="base_path resolution"; style=filled; @@ -61,7 +70,7 @@ digraph ConfigResolver { bp_coord -> bp_default [label="empty?"]; } - // --- workspace resolution --- + // --- workspace resolution (hook) --- subgraph cluster_workspace { label="workspace resolution"; style=filled; @@ -76,19 +85,35 @@ digraph ConfigResolver { ws_config -> ws_slug [label="empty?"]; } - // --- Simple properties (no fallback chain) --- + // --- destinations: the hook's named server map (write / fan-out) --- + subgraph cluster_destinations { + label="destinations (hook write side — fan-out)"; + style=filled; + fillcolor="#ede7f6"; + color="#673ab7"; + + dst_map [label="destinations:\n{ name → Destination(url, api_key,\n include, exclude) }\nnamed map, declaration order"]; + dst_legacy [label="legacy scalars\ncontext_intelligence_server_url / _api_key\n→ synthesize {\"default\": …}\nONLY when no destinations block\n(both fields required)", shape=note, fillcolor="#fff3cd"]; + dst_fanout [label="fan-out: send to EVERY destination where\ninclude matches AND exclude does not\n(exclude wins; zero / one / several)\nlocal JSONL always written"]; + dst_first [label="first destination\n= next(iter(destinations.values()), None)"]; + + dst_legacy -> dst_map [label="synthesize", style=dashed]; + dst_map -> dst_fanout [label="route"]; + dst_map -> dst_first [label="for queries"]; + } + + // --- Simple hook properties (no coordinator fallback) --- subgraph cluster_simple { - label="Direct config properties\n(no coordinator fallback)"; + label="Direct hook config properties"; style=filled; fillcolor="#f3e5f5"; color="#9c27b0"; - server_url [label="context_intelligence_server_url\nNone if empty"]; log_level [label="log_level\ndefault: INFO"]; exclude [label="exclude_events\ndefault: frozenset()"]; } - // --- Derived paths --- + // --- Derived paths (hook) --- subgraph cluster_derived { label="Derived Paths"; style=filled; @@ -99,16 +124,42 @@ digraph ConfigResolver { blob_root [label="blob_store_root\nbase_path / project_slug /\nsessions"]; } - // --- Edges from resolver --- - resolver -> ps_config [lhead=cluster_project_slug]; - resolver -> bp_config [lhead=cluster_base_path]; - resolver -> ws_coord [lhead=cluster_workspace]; - resolver -> server_url [lhead=cluster_simple]; + // --- Query endpoint resolution (read side, ToolConfigResolver) --- + // resolve_query_endpoint(hook_resolver, tool_resolver): per-field, EXPLICIT-FIRST. + subgraph cluster_query { + label="resolve_query_endpoint() (read side — per field, explicit-first)"; + style=filled; + fillcolor="#e0f2f1"; + color="#009688"; + + q1 [label="1. sources[first]\n.url / .api_key\n(explicit read override;\n absent ⇒ synthesize default from\n tool's explicit scalars)"]; + q2 [label="2. hook destinations[first]\n.url / .api_key\n(bug-fix bridge: destinations-only\n setups 'just work' for reads)"]; + q3 [label="3. env\nAMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL /\n_API_KEY\n(single canonical last-resort fallback)"]; + q_none [label="else → None\n→ configuration_error", shape=note, fillcolor="#ffcdd2"]; + + q1 -> q2 [label="empty?"]; + q2 -> q3 [label="empty?"]; + q3 -> q_none [label="empty?"]; + } + + // --- Edges from the hook resolver --- + hook_resolver -> ps_config [lhead=cluster_project_slug]; + hook_resolver -> bp_config [lhead=cluster_base_path]; + hook_resolver -> ws_coord [lhead=cluster_workspace]; + hook_resolver -> dst_map [lhead=cluster_destinations]; + hook_resolver -> log_level [lhead=cluster_simple]; - hook_config -> resolver [label="config dict"]; - coord_config -> resolver [label="coordinator"]; + hook_config -> hook_resolver [label="config dict"]; + coord_config -> hook_resolver [label="coordinator"]; session_cap -> ps_working_dir [label="working_dir", style=dashed]; + // --- Edges from the tool resolver --- + tool_resolver -> q1 [lhead=cluster_query]; + tool_config -> tool_resolver [label="config dict"]; + coord_config -> tool_resolver [label="coordinator"]; + // tier 2 reads the hook's first destination (shared model) + dst_first -> q2 [style=dashed, label="first hook destination", color="#673ab7"]; + // --- Derived path dependencies --- ps_default -> session_dir [style=dashed, label="project_slug", color="#999999"]; bp_default -> session_dir [style=dashed, label="base_path", color="#999999"]; diff --git a/context_intelligence/config.py b/context_intelligence/config.py index df45f03..d0c9a66 100644 --- a/context_intelligence/config.py +++ b/context_intelligence/config.py @@ -16,6 +16,7 @@ import logging import os +import re from pathlib import Path log = logging.getLogger("context_intelligence.config") @@ -29,6 +30,54 @@ SETTINGS_PATH = AMPLIFIER_DIR / "settings.yaml" +# --------------------------------------------------------------------------- +# Shared env-var helpers (used by HookConfigResolver and ToolConfigResolver) +# --------------------------------------------------------------------------- + +#: Environment variable prefix shared by all CI configuration. +#: ``AMPLIFIER_CONTEXT_INTELLIGENCE_WORKSPACE`` → workspace +#: ``AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL`` → context_intelligence_server_url +#: etc. +_ENV_PREFIX = "AMPLIFIER_CONTEXT_INTELLIGENCE_" + + +def _env(suffix: str) -> str | None: + """Read ``AMPLIFIER_CONTEXT_INTELLIGENCE_`` from the environment. + + Returns the value as a string if set and non-empty, otherwise ``None``. + """ + value = os.environ.get(_ENV_PREFIX + suffix) + return value if value else None + + +# --------------------------------------------------------------------------- +# Shell-style placeholder expander (used by ToolConfigResolver) +# --------------------------------------------------------------------------- + +_PLACEHOLDER_RE = re.compile(r"\$\{([^}:]+)(?::([^}]*))?\\}") + + +def _expand_env_placeholders(value: str) -> str: + """Expand shell-style ``${VAR}``, ``${VAR:}``, ``${VAR:default}`` placeholders. + + - ``${VAR}`` — replaced with ``os.environ[VAR]`` if set, else ``""``. + - ``${VAR:}`` — same as ``${VAR}`` (empty default when var is unset). + - ``${VAR:default}`` — replaced with ``os.environ[VAR]`` if set, else ``"default"``. + - Non-placeholder strings pass through unchanged. + + Note: ``os.path.expandvars`` does **not** support the ``${VAR:default}`` + colon syntax used by the agent behavior YAML files shipped with this bundle, + hence this small regex-based helper. + """ + + def _replace(m: re.Match[str]) -> str: + var_name = m.group(1) + default = m.group(2) if m.group(2) is not None else "" + return os.environ.get(var_name, default) + + return _PLACEHOLDER_RE.sub(_replace, value) + + # --------------------------------------------------------------------------- # Settings.yaml parser # --------------------------------------------------------------------------- diff --git a/context_intelligence/tool_resolver.py b/context_intelligence/tool_resolver.py new file mode 100644 index 0000000..b64435b --- /dev/null +++ b/context_intelligence/tool_resolver.py @@ -0,0 +1,266 @@ +"""ToolConfigResolver — lazy config resolver for CI tools in analytics-only mode. + +Used by the tool-context-intelligence-query module (graph_query + blob_read tools) +when the hook-context-intelligence module is NOT mounted. Constructed **eagerly** inside the tool's ``__init__`` +(both tools always create a ``ToolConfigResolver`` at construction time). Its +properties mirror HookConfigResolver for the shared config keys. + +When ``hook-context-intelligence`` IS mounted it registers a +``HookConfigResolver`` as ``context_intelligence.hook_config_resolver``; the +tools then use ``resolve_query_endpoint(hook_resolver, tool_resolver)`` which +prefers the explicit read-config over the hook's upload destinations. + +Resolution priority for every scalar property (mirrors HookConfigResolver for +the shared config keys): + 1. ``config`` dict (mount config / settings.yaml overrides) + 2. ``coordinator.config`` dict (coordinator-level overrides) + 3. ``AMPLIFIER_CONTEXT_INTELLIGENCE_*`` environment variable + 4. ``~/.amplifier/settings.yaml`` (``overrides.hook-context-intelligence…``) + +workspace resolution differs from HookConfigResolver by design: + HookConfigResolver.workspace falls back to ``project_slug`` which is + auto-derived from session.working_dir (a hook-only capability). + ToolConfigResolver.workspace falls back to the env var then ``"default"`` + because there is no session.working_dir in analytics-only mode. + +See resolve_query_endpoint() for the full three-tier fallback used by the tools. +""" + +from __future__ import annotations + +import logging +from typing import Any, NamedTuple + +from context_intelligence.config import SETTINGS_PATH, _env, _parse_settings_yaml + +log = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- + + +class Source(NamedTuple): + """A single read-config entry (mirrors Destination shape, minus upload fields). + + url and api_key may be empty strings (→ falsy → that field falls through + in the per-field resolution chain). + """ + + name: str + url: str + api_key: str + + +# --------------------------------------------------------------------------- +# Resolution helpers (free functions — shared by both tools) +# --------------------------------------------------------------------------- + + +def _first_entry(mapping: Any) -> Any | None: + """First value of an insertion-ordered ``dict``, or None. + + Defensive: returns None when *mapping* is not a non-empty dict (e.g. a test + double, or an unset attribute). Used for BOTH the tool's own ``sources`` + mapping and the hook's destinations so the 'first' rule is identical on + both sides. + """ + if not isinstance(mapping, dict) or not mapping: + return None + return next(iter(mapping.values()), None) + + +def _first_destination(hook_resolver: Any | None) -> Any | None: + """First upload Destination on the hook resolver, or None.""" + if hook_resolver is None: + return None + return _first_entry(getattr(hook_resolver, "destinations", None)) + + +def _pick(*candidates: tuple[str | None, str | None]) -> tuple[str | None, str | None]: + """Return (value, source-label) for the first non-empty candidate, else (None, None).""" + for value, source in candidates: + if value: + return value, source + return None, None + + +def resolve_query_endpoint( + hook_resolver: Any | None, + tool_resolver: "ToolConfigResolver", +) -> tuple[str | None, str | None]: + """Resolve (server_url, api_key) for the query path. Per-field independent. + + Explicit-first order (each field, first non-empty wins): + 1. first entry of tool_resolver.sources (.url / .api_key) + 2. first upload destination on the hook resolver (.url / .api_key) + 3. AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL / AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY + Returns (None, None)-able per field; each is None only if all three miss. + + Emits one DEBUG line naming which tier supplied each field. + """ + read = _first_entry(tool_resolver.sources) + dest = _first_destination(hook_resolver) + + url, url_src = _pick( + ((read.url if read else None), f"source:{read.name}" if read else None), + ((dest.url if dest else None), f"destination:{dest.name}" if dest else None), + (_env("SERVER_URL"), "env:SERVER_URL"), + ) + api_key, key_src = _pick( + ((read.api_key if read else None), f"source:{read.name}" if read else None), + ((dest.api_key if dest else None), f"destination:{dest.name}" if dest else None), + (_env("API_KEY"), "env:API_KEY"), + ) + + log.debug( + "CI query endpoint resolved: url<-%s api_key<-%s", + url_src or "none", + key_src or "none", + ) + return (url or None, api_key or None) + + +# --------------------------------------------------------------------------- +# ToolConfigResolver +# --------------------------------------------------------------------------- + + +class ToolConfigResolver: + """Lazy config resolver for CI query tools (graph-query, blob-read). + + Created eagerly at tool construction time alongside ``_hook_resolver = None``. + Provides scalar config properties that mirror HookConfigResolver and a new + ``sources`` mapping property for the explicit read-config model. + """ + + def __init__(self, config: dict[str, Any], coordinator: Any) -> None: + self._config = config + self._coordinator = coordinator + self._workspace: str | None = None + self._sources: dict[str, Source] | None = None + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + def _coordinator_config_get(self, key: str) -> Any: + """Safely read *key* from coordinator.config. + + Returns ``None`` if the coordinator has no ``.config`` attribute or + if the key is absent from it. Mirrors HookConfigResolver._coordinator_config_get. + """ + coord_config = getattr(self._coordinator, "config", None) + if not isinstance(coord_config, dict): + return None + return coord_config.get(key) + + # ------------------------------------------------------------------ + # Scalar properties (used by legacy synthesis in sources) + # ------------------------------------------------------------------ + + @property + def context_intelligence_server_url(self) -> str | None: + """Server URL: config → coordinator.config → env → settings.yaml. + + Note: the query path (resolve_query_endpoint) does NOT use this property + for env resolution. Env is consulted only at tier 3 via _env("SERVER_URL"). + Kept for PR #27 API parity. + """ + value = ( + self._config.get("context_intelligence_server_url") + or self._coordinator_config_get("context_intelligence_server_url") + or _env("SERVER_URL") + or _parse_settings_yaml(SETTINGS_PATH).get("server_url") + ) + return str(value) if value else None + + @property + def context_intelligence_api_key(self) -> str | None: + """API key: config → coordinator.config → env → settings.yaml. + + Note: the query path (resolve_query_endpoint) does NOT use this property + for env resolution. Env is consulted only at tier 3 via _env("API_KEY"). + Kept for PR #27 API parity. + """ + value = ( + self._config.get("context_intelligence_api_key") + or self._coordinator_config_get("context_intelligence_api_key") + or _env("API_KEY") + or _parse_settings_yaml(SETTINGS_PATH).get("api_key") + ) + return str(value) if value else None + + @property + def workspace(self) -> str: + """Workspace: config → coordinator.config → env → "default". + + Deliberately does NOT fall back to project_slug (which requires + session.working_dir, a hook-only capability). + """ + if self._workspace is None: + raw = ( + self._config.get("workspace") + or self._coordinator_config_get("workspace") + or _env("WORKSPACE") + or "default" + ) + self._workspace = str(raw) + return self._workspace + + # ------------------------------------------------------------------ + # Read-config mapping (the new explicit read-config model) + # ------------------------------------------------------------------ + + @property + def sources(self) -> dict[str, Source]: + """Parsed ``sources`` mapping, or legacy-synthesized ``{"default": ...}``. + + Parsing rules: + - If ``config["sources"]`` key is **present**: parse it + (may be empty dict {}). Each value must be a dict; ``url``/``api_key`` + are ``str(...).strip()``; non-dict entries are skipped. + - If key is **absent**: apply legacy synthesis. If BOTH explicit + ``context_intelligence_server_url`` and ``context_intelligence_api_key`` + are non-empty (from ``config`` dict or coordinator.config **only** — + env and settings.yaml are excluded so env cannot enter tier 1), synthesize + ``{"default": Source(...)}``. Otherwise return ``{}``. + (Mirrors the hook's destinations D10 synthesis.) + + Cached after first access. + """ + if self._sources is not None: + return self._sources + + _sentinel = object() + raw = self._config.get("sources", _sentinel) + key_present = raw is not _sentinel + + if key_present: + result: dict[str, Source] = {} + if isinstance(raw, dict): + for name, spec in raw.items(): + if not isinstance(spec, dict): + continue + url = str(spec.get("url", "") or "").strip() + api_key = str(spec.get("api_key", "") or "").strip() + result[name] = Source(name=name, url=url, api_key=api_key) + self._sources = result + return self._sources + + # Key absent: legacy synthesis from EXPLICIT config only. + # env and settings.yaml are intentionally excluded — env is consulted only + # at tier 3 in resolve_query_endpoint() so it never outranks the hook + # destination (tier 2). + legacy_url = self._config.get( + "context_intelligence_server_url" + ) or self._coordinator_config_get("context_intelligence_server_url") + legacy_key = self._config.get( + "context_intelligence_api_key" + ) or self._coordinator_config_get("context_intelligence_api_key") + if legacy_url and legacy_key: + self._sources = {"default": Source(name="default", url=legacy_url, api_key=legacy_key)} + else: + self._sources = {} + return self._sources diff --git a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/__init__.py b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/__init__.py index c07e0dc..2345e41 100644 --- a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/__init__.py +++ b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/__init__.py @@ -169,10 +169,10 @@ async def mount( """Mount the context-intelligence hook. Always: - - Registers ConfigResolver as ``context_intelligence.config_resolver`` capability + - Registers HookConfigResolver as ``context_intelligence.hook_config_resolver`` capability - LoggingHandler — writes events.jsonl + dispatches to CI server(s) """ - from .config_resolver import ConfigResolver + from .config_resolver import HookConfigResolver from .handlers.logging_handler import LoggingHandler from .skill_fetcher import ( TOOL_SKILLS_DISCOVERY_CAPABILITY, @@ -181,9 +181,9 @@ async def mount( _is_skills_capable, ) - resolver = ConfigResolver(config, coordinator) + resolver = HookConfigResolver(config, coordinator) log.setLevel(resolver.log_level) - coordinator.register_capability("context_intelligence.config_resolver", resolver) + coordinator.register_capability("context_intelligence.hook_config_resolver", resolver) unregister_fns: list[Callable[[], None]] = [] @@ -286,7 +286,7 @@ async def cleanup() -> None: except Exception: pass try: - coordinator.register_capability("context_intelligence.config_resolver", None) + coordinator.register_capability("context_intelligence.hook_config_resolver", None) except Exception: pass try: diff --git a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py index 80f5be3..1524fff 100644 --- a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py +++ b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py @@ -71,7 +71,7 @@ def _slugify_path(path_str: str) -> str: return slug or _DEFAULT_PROJECT_SLUG -class ConfigResolver: +class HookConfigResolver: """Resolve configuration values with lazy fallback chains. Resolution order per property: @@ -155,6 +155,21 @@ def project_slug(self) -> str: self._project_slug = str(raw) return self._project_slug + @property + def working_dir(self) -> str: + """Absolute session working directory from the ``session.working_dir`` capability. + + Read live (not cached) so it reflects mid-session working-directory changes. + Returns "" when the capability is unavailable. + """ + get_cap = getattr(self._coordinator, "get_capability", None) + if get_cap is None: + return "" + wd = get_cap("session.working_dir") + if not isinstance(wd, str) or not wd: + return "" + return wd + @property def base_path(self) -> Path: """Resolved base path for project storage. @@ -453,3 +468,9 @@ def validate_destinations(self) -> dict[str, Destination]: f"overrides.hook-context-intelligence.config.destinations.." ) return dests + + +# --------------------------------------------------------------------------- +# Backward-compat alias — import either name (HookConfigResolver is canonical) +# --------------------------------------------------------------------------- +ConfigResolver = HookConfigResolver diff --git a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/handlers/logging_handler.py b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/handlers/logging_handler.py index bada232..166b5e5 100644 --- a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/handlers/logging_handler.py +++ b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/handlers/logging_handler.py @@ -299,7 +299,7 @@ def _ensure_metadata( "started_at": data.get("timestamp", ""), "last_event_at": data.get("timestamp", ""), "status": "running", - "working_dir": data.get("working_dir", ""), + "working_dir": self._resolver.working_dir, } meta_path.write_text(json.dumps(metadata, separators=(",", ":"))) @@ -334,7 +334,7 @@ def _enrich_metadata_from_session_init( or meta.get("parent_id", "") ) meta["started_at"] = data.get("timestamp", "") or meta.get("started_at", "") - meta["working_dir"] = data.get("working_dir", "") or meta.get("working_dir", "") + meta["working_dir"] = self._resolver.working_dir or meta.get("working_dir", "") for field in _OPTIONAL_METADATA_FIELDS: value = data.get(field) diff --git a/modules/hook-context-intelligence/tests/test_config_resolver.py b/modules/hook-context-intelligence/tests/test_config_resolver.py index 13e9ceb..ff20b10 100644 --- a/modules/hook-context-intelligence/tests/test_config_resolver.py +++ b/modules/hook-context-intelligence/tests/test_config_resolver.py @@ -1,11 +1,13 @@ -"""Tests for ConfigResolver resolution chains.""" +"""Tests for HookConfigResolver resolution chains.""" from pathlib import Path from unittest.mock import MagicMock import fnmatch -from amplifier_module_hook_context_intelligence.config_resolver import ConfigResolver +from amplifier_module_hook_context_intelligence.config_resolver import ( + HookConfigResolver as ConfigResolver, +) # noqa: PLC0414 from amplifier_module_hook_context_intelligence.config_resolver import _slugify_path @@ -700,3 +702,86 @@ def test_handles_windows_path(self) -> None: def test_empty_string_returns_default(self) -> None: """Empty path string returns the default project slug.""" assert _slugify_path("") == "default" + + +class TestWorkingDir: + """working_dir property — live read of the session.working_dir capability. + + Unlike project_slug (cached), working_dir is read live on every access so it + reflects mid-session working-directory changes. It returns "" when the + capability is unavailable. + """ + + def test_returns_capability_value_when_present(self) -> None: + """working_dir returns the path string from the session.working_dir capability.""" + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value="/home/user/project") + resolver = ConfigResolver(config={}, coordinator=coordinator) + + assert resolver.working_dir == "/home/user/project" + + def test_returns_empty_string_when_capability_returns_none(self) -> None: + """working_dir returns '' when get_capability('session.working_dir') is None.""" + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value=None) + resolver = ConfigResolver(config={}, coordinator=coordinator) + + assert resolver.working_dir == "" + + def test_returns_empty_string_when_capability_returns_empty_string(self) -> None: + """working_dir returns '' when the capability returns '' (present-but-empty).""" + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value="") + resolver = ConfigResolver(config={}, coordinator=coordinator) + + assert resolver.working_dir == "" + + def test_returns_empty_string_when_capability_returns_non_str(self) -> None: + """working_dir returns '' when the capability returns a non-string type.""" + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value=42) + resolver = ConfigResolver(config={}, coordinator=coordinator) + + assert resolver.working_dir == "" + + def test_returns_empty_string_when_coordinator_lacks_get_capability(self) -> None: + """working_dir returns '' when the coordinator has no get_capability method.""" + bare = _make_bare_coordinator() # plain object(), no get_capability + resolver = ConfigResolver(config={}, coordinator=bare) + + assert resolver.working_dir == "" + + def test_returns_empty_string_when_coordinator_is_none(self) -> None: + """working_dir returns '' when coordinator is None.""" + resolver = ConfigResolver(config={}, coordinator=None) + + assert resolver.working_dir == "" + + def test_not_cached_reads_live(self) -> None: + """working_dir is NOT cached — each access re-reads the capability. + + This is intentional: working_dir must reflect mid-session cwd changes, + unlike project_slug which is cached after first access. + """ + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value="/first/path") + resolver = ConfigResolver(config={}, coordinator=coordinator) + + first = resolver.working_dir + assert first == "/first/path" + + # Simulate capability value changing mid-session. + coordinator.get_capability = MagicMock(return_value="/second/path") + second = resolver.working_dir + assert second == "/second/path" + + # Contrast with project_slug which IS cached. + assert first != second + + def test_returns_str_type(self) -> None: + """working_dir always returns a str.""" + coordinator = MagicMock() + coordinator.get_capability = MagicMock(return_value="/some/path") + resolver = ConfigResolver(config={}, coordinator=coordinator) + + assert isinstance(resolver.working_dir, str) diff --git a/modules/hook-context-intelligence/tests/test_logging_handler.py b/modules/hook-context-intelligence/tests/test_logging_handler.py index 904897e..0847ce8 100644 --- a/modules/hook-context-intelligence/tests/test_logging_handler.py +++ b/modules/hook-context-intelligence/tests/test_logging_handler.py @@ -20,11 +20,16 @@ class _FakeResolver: """Minimal resolver adapter for testing LoggingHandler in isolation.""" def __init__( - self, base_path: Path, project_slug: str, workspace: str = "test-workspace" + self, + base_path: Path, + project_slug: str, + workspace: str = "test-workspace", + working_dir: str = "", ) -> None: self.base_path = base_path self.project_slug = project_slug self.workspace = workspace + self.working_dir = working_dir def session_dir(self, session_id: str) -> Path: return self.base_path / self.project_slug / "sessions" / session_id / "context-intelligence" @@ -91,14 +96,19 @@ async def test_writes_metadata_json_with_correct_fields(self, tmp_path: Path) -> LoggingHandler, ) - handler = LoggingHandler(_FakeResolver(tmp_path, "proj", workspace="my-project")) + # working_dir comes from the resolver (session capability), not the event payload. + handler = LoggingHandler( + _FakeResolver( + tmp_path, "proj", workspace="my-project", working_dir="/home/user/project" + ) + ) await handler( "session:start", { "session_id": "s1", "parent_id": "p1", "timestamp": "2026-01-15T10:00:00Z", - "working_dir": "/home/user/project", + # working_dir in event data is NOT used for metadata; resolver value is. }, ) meta_path = tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" @@ -646,3 +656,178 @@ async def test_event_data_parent_id_wins_over_config(self, tmp_path: Path) -> No ).read_text() ) assert meta["parent_id"] == "from-event" + + +# --------------------------------------------------------------------------- +# TestWorkingDirFromResolver — working_dir is a session attribute, not event data +# --------------------------------------------------------------------------- +class TestWorkingDirFromResolver: + """working_dir in metadata.json comes from the resolver (session capability), not the event. + + This class covers three key guarantees: + (a) working_dir is populated from the resolver even when the first event is NOT + session:start and carries no working_dir in its payload. + (b) A present-but-empty working_dir in the event payload does NOT win — the resolver + value is used (event-payload empty string was the old bug). + (c) When the resolver's working_dir is "" (capability unavailable), metadata gracefully + stores "", and _enrich does not clobber a previously-set non-empty value. + """ + + async def test_working_dir_from_resolver_on_non_session_start_first_event( + self, tmp_path: Path + ) -> None: + """(a) working_dir comes from resolver when first event is tool:call (no session:start).""" + from amplifier_module_hook_context_intelligence.handlers.logging_handler import ( + LoggingHandler, + ) + + resolver = _FakeResolver(tmp_path, "proj", working_dir="/resolver/working/dir") + handler = LoggingHandler(resolver) + + # First event is NOT session:start — old code would have stored "" here. + await handler( + "tool:call", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:01Z", + "tool_name": "read_file", + # No working_dir in payload at all. + }, + ) + + meta = json.loads( + ( + tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" + ).read_text() + ) + assert meta["working_dir"] == "/resolver/working/dir" + + async def test_present_but_empty_event_working_dir_does_not_win(self, tmp_path: Path) -> None: + """(b) An empty working_dir in the event payload does not clobber the resolver value. + + The old code used data.get("working_dir", ""), so a payload that carries + {"working_dir": ""} (present-but-empty) would return "" and store it. + New code ignores event working_dir entirely; the resolver always wins. + """ + from amplifier_module_hook_context_intelligence.handlers.logging_handler import ( + LoggingHandler, + ) + + resolver = _FakeResolver(tmp_path, "proj", working_dir="/real/path") + handler = LoggingHandler(resolver) + + # Event deliberately carries working_dir="" (present-but-empty). + await handler( + "session:start", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:00Z", + "working_dir": "", # Present but empty — old code would have stored "". + }, + ) + + meta = json.loads( + ( + tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" + ).read_text() + ) + # Resolver's value wins over the event's empty string. + assert meta["working_dir"] == "/real/path" + + async def test_resolver_working_dir_empty_stores_empty_gracefully(self, tmp_path: Path) -> None: + """(c-i) When resolver.working_dir is '', metadata working_dir is '' (graceful).""" + from amplifier_module_hook_context_intelligence.handlers.logging_handler import ( + LoggingHandler, + ) + + resolver = _FakeResolver(tmp_path, "proj", working_dir="") + handler = LoggingHandler(resolver) + + await handler( + "session:start", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:00Z", + }, + ) + + meta = json.loads( + ( + tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" + ).read_text() + ) + assert meta["working_dir"] == "" + + async def test_enrich_does_not_clobber_prior_value_with_empty_resolver( + self, tmp_path: Path + ) -> None: + """(c-ii) _enrich does not overwrite a good working_dir with '' from an empty resolver. + + Scenario: first event stores a non-empty working_dir via the resolver, then + _enrich_metadata_from_session_init is called (session:start arrives late) while + the resolver's working_dir returns "". The prior value must be preserved. + """ + from amplifier_module_hook_context_intelligence.handlers.logging_handler import ( + LoggingHandler, + ) + + # Phase 1: resolver has a working_dir — first event stores it. + resolver = _FakeResolver(tmp_path, "proj", working_dir="/first/event/dir") + handler = LoggingHandler(resolver) + + await handler( + "tool:call", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:01Z", + }, + ) + + # Verify initial state. + meta_path = tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" + meta = json.loads(meta_path.read_text()) + assert meta["working_dir"] == "/first/event/dir" + + # Phase 2: resolver now returns "" (e.g. capability went away). + resolver.working_dir = "" + + # session:start arrives (triggers _enrich). + await handler( + "session:start", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:02Z", + # No working_dir in event payload either. + }, + ) + + meta = json.loads(meta_path.read_text()) + # Prior non-empty value must be preserved; "" from resolver must not clobber. + assert meta["working_dir"] == "/first/event/dir" + + async def test_working_dir_from_resolver_overrides_event_on_session_start( + self, tmp_path: Path + ) -> None: + """Resolver value wins even when event carries a different non-empty working_dir.""" + from amplifier_module_hook_context_intelligence.handlers.logging_handler import ( + LoggingHandler, + ) + + resolver = _FakeResolver(tmp_path, "proj", working_dir="/resolver/path") + handler = LoggingHandler(resolver) + + await handler( + "session:start", + { + "session_id": "s1", + "timestamp": "2026-01-15T10:00:00Z", + "working_dir": "/event/path", # Different value in event — resolver wins. + }, + ) + + meta = json.loads( + ( + tmp_path / "proj" / "sessions" / "s1" / "context-intelligence" / "metadata.json" + ).read_text() + ) + assert meta["working_dir"] == "/resolver/path" diff --git a/modules/hook-context-intelligence/tests/test_logging_handler_fanout.py b/modules/hook-context-intelligence/tests/test_logging_handler_fanout.py index 46e996b..6378968 100644 --- a/modules/hook-context-intelligence/tests/test_logging_handler_fanout.py +++ b/modules/hook-context-intelligence/tests/test_logging_handler_fanout.py @@ -21,6 +21,7 @@ def __init__(self, base_path: Path, project_slug: str = "proj") -> None: self.workspace: str | None = "ws" self.parent_id: str = "" self.resolve_instance_id: str = "" + self.working_dir: str = "" def session_dir(self, session_id: str) -> Path: return self.base_path / self.project_slug / "sessions" / session_id / "context-intelligence" diff --git a/modules/hook-context-intelligence/tests/test_logging_handler_last_event_at.py b/modules/hook-context-intelligence/tests/test_logging_handler_last_event_at.py index 2f9b65e..febda7f 100644 --- a/modules/hook-context-intelligence/tests/test_logging_handler_last_event_at.py +++ b/modules/hook-context-intelligence/tests/test_logging_handler_last_event_at.py @@ -22,6 +22,7 @@ def __init__( self.base_path = base_path self.project_slug = project_slug self.workspace = workspace + self.working_dir: str = "" def session_dir(self, session_id: str) -> Path: return self.base_path / self.project_slug / "sessions" / session_id / "context-intelligence" diff --git a/modules/hook-context-intelligence/tests/test_logging_handler_server_dispatch.py b/modules/hook-context-intelligence/tests/test_logging_handler_server_dispatch.py index efead83..1aa5922 100644 --- a/modules/hook-context-intelligence/tests/test_logging_handler_server_dispatch.py +++ b/modules/hook-context-intelligence/tests/test_logging_handler_server_dispatch.py @@ -42,6 +42,7 @@ def __init__( self.workspace = workspace self.parent_id = parent_id self.resolve_instance_id = resolve_instance_id + self.working_dir: str = "" def session_dir(self, session_id: str) -> Path: return self.base_path / self.project_slug / "sessions" / session_id / "context-intelligence" diff --git a/modules/hook-context-intelligence/tests/test_mount_dispatcher.py b/modules/hook-context-intelligence/tests/test_mount_dispatcher.py index af765bf..99c8e02 100644 --- a/modules/hook-context-intelligence/tests/test_mount_dispatcher.py +++ b/modules/hook-context-intelligence/tests/test_mount_dispatcher.py @@ -326,22 +326,24 @@ def test_mount_signature_has_coordinator_and_config(self) -> None: # TestCapabilityRegistration # --------------------------------------------------------------------------- class TestCapabilityRegistration: - """Hook registers ConfigResolver as a coordinator capability.""" + """Hook registers HookConfigResolver as a coordinator capability.""" async def test_config_resolver_capability_registered_on_mount(self) -> None: - """mount() registers the config_resolver capability with a ConfigResolver instance.""" + """mount() registers the hook_config_resolver capability with a HookConfigResolver instance.""" from amplifier_module_hook_context_intelligence import mount - from amplifier_module_hook_context_intelligence.config_resolver import ConfigResolver + from amplifier_module_hook_context_intelligence.config_resolver import HookConfigResolver coordinator = _make_coordinator() await mount(coordinator, config={}) reg_calls = coordinator.register_capability.call_args_list - cap_calls = [c for c in reg_calls if c.args[0] == "context_intelligence.config_resolver"] + cap_calls = [ + c for c in reg_calls if c.args[0] == "context_intelligence.hook_config_resolver" + ] assert len(cap_calls) == 1, ( - "register_capability should be called once with 'context_intelligence.config_resolver'" + "register_capability should be called once with 'context_intelligence.hook_config_resolver'" ) - assert isinstance(cap_calls[0].args[1], ConfigResolver) + assert isinstance(cap_calls[0].args[1], HookConfigResolver) async def test_hook_state_capability_registered_on_mount(self) -> None: """mount() registers the _hook_state capability as a dict with required keys.""" @@ -375,5 +377,5 @@ async def test_cleanup_vacates_both_capabilities(self) -> None: null_calls: dict[str, Any] = { c.args[0]: c.args[1] for c in coordinator.register_capability.call_args_list } - assert null_calls["context_intelligence.config_resolver"] is None + assert null_calls["context_intelligence.hook_config_resolver"] is None assert null_calls["context_intelligence._hook_state"] is None diff --git a/modules/hook-context-intelligence/tests/test_silent_by_default.py b/modules/hook-context-intelligence/tests/test_silent_by_default.py index 13b4c60..20d1059 100644 --- a/modules/hook-context-intelligence/tests/test_silent_by_default.py +++ b/modules/hook-context-intelligence/tests/test_silent_by_default.py @@ -30,6 +30,7 @@ def _make_resolver(tmp_path, *, server_url=None, api_key=None): context_intelligence_server_url=server_url, context_intelligence_api_key=api_key, workspace="test-workspace", + working_dir="", dispatch_timeout=10.0, dispatch_failure_threshold=3, dispatch_queue_capacity=256, diff --git a/modules/hook-context-intelligence/uv.lock b/modules/hook-context-intelligence/uv.lock index b81a47e..d76de56 100644 --- a/modules/hook-context-intelligence/uv.lock +++ b/modules/hook-context-intelligence/uv.lock @@ -5,12 +5,12 @@ requires-python = ">=3.11" [[package]] name = "amplifier-bundle-context-intelligence" version = "0.1.1" -source = { git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=v0.1.1#b722074f17a354816ebf5adcf0881b1562a2cbc5" } +source = { git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=main#2e47dc7c331b60210814f711129a10fdffd76ee4" } [[package]] name = "amplifier-core" -version = "1.4.1" -source = { git = "https://github.com/microsoft/amplifier-core?rev=v1.4.1#12f3d383de723993c9d15cf61d8f0f77e5e874d4" } +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "pydantic" }, @@ -18,6 +18,14 @@ dependencies = [ { name = "tomli" }, { name = "typing-extensions" }, ] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/90/d520390cd91aae3d02db53653f828046089c79203dbb142e9bda346fa1d6/amplifier_core-1.6.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d35130e4262cf0db2d6c5f7e65e244a9ef2c7397bfe2a9853bc9b0d9fd05be64", size = 8113151, upload-time = "2026-05-18T16:13:46.825Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3ab3126ba5a6f2fc6051a4d08e42364899e4c9ac4daa9d0a60947bf8acd1/amplifier_core-1.6.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:387a2c58fcf4caefdb45c52ec228307bc225e73606897f242154782bc3e123da", size = 7268223, upload-time = "2026-05-18T16:13:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/21/22/5a36160b3487170bcba0cbc61535101ff624e8314ed38fd35e561cb711a1/amplifier_core-1.6.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8344fccdedd725a51c018de17867cdf1c35abb571dabc0bbccdb5c1242324a47", size = 7532259, upload-time = "2026-05-18T16:13:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/3874c2308523209411367cf3b8b690e14e869f5f6bfb64cb1b1971e06a96/amplifier_core-1.6.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a8e0103242a2e2a975c880b1de0e5a02501e0421c1e5386dadae3f111e1d2b5", size = 8507642, upload-time = "2026-05-18T16:13:52.977Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/3646a89537b4556274183519f6db9c354fb3d183f52ef4a2179af12dd386/amplifier_core-1.6.0-cp311-abi3-win_amd64.whl", hash = "sha256:5113aa2d88038776eb257af9e7d9de7af13b3cd9097d2ac67aef5730fa0678e3", size = 8910313, upload-time = "2026-05-18T16:13:55.249Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/58b141115e5eea65703f0b01459eefed36b561e9642ba96d48542345cd8f/amplifier_core-1.6.0-cp311-abi3-win_arm64.whl", hash = "sha256:e1b2731dc09d1cbc668b411007e7f9a2c7edbd75b2525407cae1e6b4a4de0b83", size = 7661416, upload-time = "2026-05-18T16:13:57.513Z" }, +] [[package]] name = "amplifier-module-hook-context-intelligence" @@ -42,7 +50,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "amplifier-bundle-context-intelligence", git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=v0.1.1" }, + { name = "amplifier-bundle-context-intelligence", git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=main" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "idna", specifier = ">=3.15" }, { name = "pathspec", specifier = ">=0.12,<2" }, @@ -50,7 +58,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "amplifier-core", git = "https://github.com/microsoft/amplifier-core?rev=v1.4.1" }, + { name = "amplifier-core", specifier = ">=1.6.0" }, { name = "pyright", specifier = ">=1.1" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=0.24" }, diff --git a/modules/tool-blob-read/amplifier_module_tool_blob_read/__init__.py b/modules/tool-blob-read/amplifier_module_tool_blob_read/__init__.py deleted file mode 100644 index 9e15876..0000000 --- a/modules/tool-blob-read/amplifier_module_tool_blob_read/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Blob read tool module — reads binary/text blobs from the context-intelligence server. - -Implements the Amplifier Tool protocol. Configuration is resolved lazily -via the ``context_intelligence.config_resolver`` coordinator capability -registered by the hook-context-intelligence module. -""" - -from __future__ import annotations - -from typing import Any - -__amplifier_module_type__ = "tool" - - -async def mount(coordinator: Any, config: Any) -> dict[str, Any]: # noqa: ARG001 - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(coordinator) - await coordinator.mount("tools", tool, name=tool.name) - return {"tool": tool.name, "status": "mounted"} diff --git a/modules/tool-blob-read/pyproject.toml b/modules/tool-blob-read/pyproject.toml deleted file mode 100644 index c6e935f..0000000 --- a/modules/tool-blob-read/pyproject.toml +++ /dev/null @@ -1,52 +0,0 @@ -[project] -name = "amplifier-module-tool-blob-read" -version = "0.1.0" -description = "Blob read tool — reads binary/text blobs from the context-intelligence server" -requires-python = ">=3.11" -license = "MIT" - -dependencies = [ - "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main", - "httpx>=0.28.1", - "idna>=3.15", -] - -[project.entry-points."amplifier.modules"] -tool-blob-read = "amplifier_module_tool_blob_read:mount" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -package = true - -[tool.hatch.build.targets.wheel] -packages = ["amplifier_module_tool_blob_read"] - -[tool.hatch.metadata] -# Required to build a wheel that carries a PEP 508 direct-reference (git+https) dependency. -allow-direct-references = true - -[dependency-groups] -dev = [ - "amplifier-core>=1.6.0", - "pytest>=9.0.3", - "pytest-asyncio>=0.24", - "pyright>=1.1", - "ruff>=0.4", -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" - -[tool.pyright] -pythonVersion = "3.11" -typeCheckingMode = "basic" -venvPath = "." -venv = ".venv" - -[tool.ruff] -target-version = "py311" -line-length = 100 diff --git a/modules/tool-blob-read/tests/test_blob_read_tool.py b/modules/tool-blob-read/tests/test_blob_read_tool.py deleted file mode 100644 index 856ecd7..0000000 --- a/modules/tool-blob-read/tests/test_blob_read_tool.py +++ /dev/null @@ -1,407 +0,0 @@ -"""Tests for BlobReadTool implementation. - -All tests mock ``amplifier_module_tool_blob_read.blob_read_tool.AsyncCIClient`` -so HTTP transport is never exercised in unit tests. -""" - -from __future__ import annotations - -import json -import pathlib -import shutil -from contextlib import contextmanager -from typing import Any, Generator -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from amplifier_core import ToolResult - -# --------------------------------------------------------------------------- -# Module-level helper functions (NO conftest.py, NO pytest fixtures) -# --------------------------------------------------------------------------- - - -def _make_coordinator(resolver: Any) -> MagicMock: - """Return a MagicMock coordinator whose get_capability returns *resolver*.""" - coordinator = MagicMock() - coordinator.get_capability.return_value = resolver - return coordinator - - -def _make_resolver(server_url: str | None, api_key: str | None = None) -> MagicMock: - """Return a MagicMock resolver with context_intelligence_server_url set. - - api_key defaults to None so tests that don't exercise auth get a clean mock. - """ - resolver = MagicMock() - resolver.context_intelligence_server_url = server_url - resolver.context_intelligence_api_key = api_key - return resolver - - -@contextmanager -def _patch_async_client( - fetch_blob_return: Any = None, -) -> Generator[tuple[MagicMock, MagicMock], None, None]: - """Context manager that patches AsyncCIClient in the blob_read_tool module. - - Yields ``(mock_cls, mock_instance)`` so callers can assert constructor - arguments (via ``mock_cls``) and ``fetch_blob`` call arguments (via - ``mock_instance``). - """ - mock_instance = MagicMock() - mock_instance.fetch_blob = AsyncMock(return_value=fetch_blob_return) - mock_cls = MagicMock(return_value=mock_instance) - with patch( - "amplifier_module_tool_blob_read.blob_read_tool.AsyncCIClient", - mock_cls, - ): - yield mock_cls, mock_instance - - -# --------------------------------------------------------------------------- -# Module-level fixture -# --------------------------------------------------------------------------- - - -@pytest.fixture(autouse=True) -def _clean_blob_dir(): # type: ignore[no-untyped-def] - """Remove /tmp/ci-blobs/ before each test to prevent cross-test pollution.""" - blob_dir = pathlib.Path("/tmp/ci-blobs") - if blob_dir.exists(): - shutil.rmtree(blob_dir) - yield - # Leave files after test for debugging; next test's setup cleans up - - -# --------------------------------------------------------------------------- -# (1) Protocol surface -# --------------------------------------------------------------------------- - - -class TestBlobReadToolProtocol: - """Tool protocol surface: name, description, schema, execute return type.""" - - def test_name_is_blob_read(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert tool.name == "blob_read" - - def test_description_mentions_ci_blob_scheme(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert "ci-blob://" in tool.description - - def test_description_mentions_file_path(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert "file path" in tool.description.lower() - assert "disk path" not in tool.description.lower() - - def test_schema_type_is_object(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert tool.input_schema["type"] == "object" - - def test_schema_has_uri_required(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert "uri" in tool.input_schema["required"] - - def test_schema_uri_is_string(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - assert tool.input_schema["properties"]["uri"]["type"] == "string" - - async def test_execute_returns_tool_result(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - tool = BlobReadTool(_make_coordinator(None)) - result = await tool.execute({"uri": "ci-blob://session/key"}) - assert isinstance(result, ToolResult) - - -# --------------------------------------------------------------------------- -# (2) Lazy capability resolution -# --------------------------------------------------------------------------- - - -class TestLazyCapabilityResolution: - """execute() must resolve the config capability lazily and cache it.""" - - async def test_capability_not_found_returns_configuration_error(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - # get_capability returns None → capability not registered - tool = BlobReadTool(_make_coordinator(None)) - result = await tool.execute({"uri": "ci-blob://session/key"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "configuration_error" - assert "not configured" in result.error["message"].lower() - - async def test_server_url_none_returns_configuration_error_with_url(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver(None) - tool = BlobReadTool(_make_coordinator(resolver)) - result = await tool.execute({"uri": "ci-blob://session/key"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "configuration_error" - assert "url" in result.error["message"].lower() - - async def test_server_url_empty_returns_configuration_error(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("") - tool = BlobReadTool(_make_coordinator(resolver)) - result = await tool.execute({"uri": "ci-blob://session/key"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "configuration_error" - - async def test_resolver_cached_after_first_lookup(self) -> None: - """get_capability should be called exactly once across two execute() calls.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - coordinator = _make_coordinator(resolver) - tool = BlobReadTool(coordinator) - - with _patch_async_client(fetch_blob_return={"data": "first"}): - await tool.execute({"uri": "ci-blob://session/key1"}) - await tool.execute({"uri": "ci-blob://session/key2"}) - - coordinator.get_capability.assert_called_once() - - -# --------------------------------------------------------------------------- -# (3) URI parsing -# --------------------------------------------------------------------------- - - -class TestURIParsing: - """execute() must validate and parse ci-blob:// URIs before fetching.""" - - async def test_missing_scheme_returns_uri_error(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - # No "ci-blob://" prefix at all - result = await tool.execute({"uri": "session/key"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "uri_error" - assert "ci-blob://" in result.error["message"] - - async def test_no_slash_after_scheme_returns_uri_error(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - # Scheme present but no "/" separating session_id from key - result = await tool.execute({"uri": "ci-blob://sessiononly"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "uri_error" - assert "session_id/key" in result.error["message"] - - async def test_valid_uri_extracts_session_and_key(self) -> None: - """A well-formed URI must call fetch_blob with correct session_id and key.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"ok": True}) as (_, mock_instance): - await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - mock_instance.fetch_blob.assert_called_once_with("my-session", "my-key") - - -# --------------------------------------------------------------------------- -# (4) Path sanitization -# --------------------------------------------------------------------------- - - -class TestPathSanitization: - """Output file paths must be confined to /tmp/ci-blobs/ regardless of key.""" - - async def test_slashes_sanitized(self) -> None: - """Slashes in the key must not create unexpected subdirectory depth.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"data": "test"}): - result = await tool.execute({"uri": "ci-blob://my-session/path/with/slashes"}) - - assert result.success is True - assert result.output is not None - output = str(result.output["path"]) - assert output.startswith("/tmp/ci-blobs/") - assert ".." not in output - - async def test_special_chars_sanitized(self) -> None: - """Path traversal sequences in the key must be neutralized in the output path.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"data": "test"}): - result = await tool.execute({"uri": "ci-blob://my-session/../../etc/passwd"}) - - assert result.success is True - assert result.output is not None - output = str(result.output["path"]) - assert output.startswith("/tmp/ci-blobs/") - - async def test_path_traversal_neutralized(self) -> None: - """Output path must always stay under /tmp/ci-blobs/ even with traversal key.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"data": "test"}): - result = await tool.execute({"uri": "ci-blob://my-session/../../etc/passwd"}) - - assert result.success is True - assert result.output is not None - output = str(result.output["path"]) - # Resolved path must be strictly under /tmp/ci-blobs/ - assert output.startswith("/tmp/ci-blobs/") - # Must not have escaped to /etc/ - assert "/etc/" not in output - - -# --------------------------------------------------------------------------- -# (5) Successful blob read -# --------------------------------------------------------------------------- - - -class TestBlobReadSuccess: - """Happy-path behaviour: file written to disk and path returned.""" - - async def test_successful_fetch_writes_file_and_returns_path(self) -> None: - """Dict blob must be written as json.dumps(data); output is the file path.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - blob_data = {"session": "test", "data": [1, 2, 3]} - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return=blob_data): - result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - assert result.success is True - assert result.output is not None - written = pathlib.Path(result.output["path"]) - assert written.exists() - assert json.loads(written.read_text()) == blob_data - - async def test_output_path_structure(self) -> None: - """Parent directory name == session_id, filename == key.json.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"ok": True}): - result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - assert result.success is True - assert result.output is not None - p = pathlib.Path(result.output["path"]) - assert p.parent.name == "my-session" - assert p.name == "my-key.json" - - async def test_string_blob_written_directly(self) -> None: - """String blob must be written as raw text, NOT wrapped in json.dumps.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - raw_string = "hello world, this is raw text" - - with _patch_async_client(fetch_blob_return=raw_string): - result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - assert result.success is True - assert result.output is not None - written = pathlib.Path(result.output["path"]) - assert written.exists() - # Must be written verbatim — not json.dumps'd (which would add quotes) - assert written.read_text() == raw_string - - -# --------------------------------------------------------------------------- -# (6) Error handling -# --------------------------------------------------------------------------- - - -class TestBlobReadErrors: - """execute() maps fetch_blob returning None to a typed http_error.""" - - async def test_fetch_blob_none_returns_http_error(self) -> None: - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return=None): - result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "http_error" - - -# --------------------------------------------------------------------------- -# (7) Authorization / constructor arguments -# --------------------------------------------------------------------------- - - -class TestAuthHeader: - """AsyncCIClient must receive the correct api_key from the resolver.""" - - async def test_api_key_passed_to_async_ci_client(self) -> None: - """When api_key is set the AsyncCIClient must be constructed with it.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080", api_key="my-secret") - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): - await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - mock_cls.assert_called_once_with(server_url="http://localhost:8080", api_key="my-secret") - - async def test_none_api_key_passes_empty_string(self) -> None: - """When api_key is None the AsyncCIClient must receive an empty string.""" - from amplifier_module_tool_blob_read.blob_read_tool import BlobReadTool - - resolver = _make_resolver("http://localhost:8080", api_key=None) - tool = BlobReadTool(_make_coordinator(resolver)) - - with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): - await tool.execute({"uri": "ci-blob://my-session/my-key"}) - - mock_cls.assert_called_once_with(server_url="http://localhost:8080", api_key="") diff --git a/modules/tool-blob-read/tests/test_mount.py b/modules/tool-blob-read/tests/test_mount.py deleted file mode 100644 index 738be38..0000000 --- a/modules/tool-blob-read/tests/test_mount.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Tests for tool-blob-read module mount contract.""" - -from __future__ import annotations - -import inspect -from unittest.mock import AsyncMock, MagicMock - - -class TestModuleContract: - """Module-level contract for a tool module.""" - - def test_module_type_is_tool(self) -> None: - from amplifier_module_tool_blob_read import __amplifier_module_type__ - - assert __amplifier_module_type__ == "tool" - - def test_mount_is_coroutine(self) -> None: - from amplifier_module_tool_blob_read import mount - - assert inspect.iscoroutinefunction(mount) - - def test_mount_signature_has_coordinator_and_config(self) -> None: - from amplifier_module_tool_blob_read import mount - - sig = inspect.signature(mount) - params = list(sig.parameters.keys()) - assert params[0] == "coordinator" - assert params[1] == "config" - - -class TestMountBehavior: - """mount() registers a Tool-protocol-compliant object via coordinator.mount().""" - - async def test_mount_calls_coordinator_mount_with_tools_category(self) -> None: - from amplifier_module_tool_blob_read import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - coordinator.mount.assert_called_once() - assert coordinator.mount.call_args.args[0] == "tools" - - async def test_mounted_tool_has_name_blob_read(self) -> None: - from amplifier_module_tool_blob_read import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - assert coordinator.mount.call_args.kwargs["name"] == "blob_read" - - async def test_mounted_tool_is_protocol_compliant(self) -> None: - from amplifier_module_tool_blob_read import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - tool = coordinator.mount.call_args.args[1] - assert hasattr(tool, "name") - assert hasattr(tool, "description") - assert hasattr(tool, "input_schema") - assert hasattr(tool, "execute") - assert isinstance(tool.input_schema, dict) - assert inspect.iscoroutinefunction(tool.execute) - - async def test_mount_returns_metadata_dict(self) -> None: - from amplifier_module_tool_blob_read import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - result = await mount(coordinator, config={}) - assert isinstance(result, dict) - assert "tool" in result - assert "status" in result diff --git a/modules/tool-blob-read/tests/test_tool_dependencies.py b/modules/tool-blob-read/tests/test_tool_dependencies.py deleted file mode 100644 index 147bf00..0000000 --- a/modules/tool-blob-read/tests/test_tool_dependencies.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Test that the blob-read tool pyproject.toml declares the bundle dependency in a -form that installs standalone (outside the monorepo). - -The tool imports from the `context_intelligence` package shipped by the parent -bundle. For the tool to install standalone under the Amplifier agent's -`uv pip install --no-sources` policy, the bundle MUST be referenced as a PEP 508 -direct git reference inside [project.dependencies] (which survives --no-sources), -NOT via a [tool.uv.sources] `path = "../.."` entry (which --no-sources strips). - -This guard mirrors hook-context-intelligence/tests/test_hook_dependencies.py so -that every module references the parent bundle uniformly and cannot regress to a -uv path source (the original cause of the uv conflicting-URLs co-install failure). -""" - -from __future__ import annotations - -import tomllib -from pathlib import Path - -MODULE_ROOT = Path(__file__).parent.parent -PYPROJECT = MODULE_ROOT / "pyproject.toml" - -BUNDLE = "amplifier-bundle-context-intelligence" - - -def _load_pyproject() -> dict: - return tomllib.loads(PYPROJECT.read_text()) - - -def _dep_name(dep: str) -> str: - """Extract the bare package name from a requirement string. - - Handles version specifiers (>=, ==) and PEP 508 direct references - (`name @ git+https://...`). - """ - return dep.split("@")[0].split(">=")[0].split("==")[0].strip() - - -class TestToolDependencies: - """Verify the tool declares the bundle as a standalone-installable dependency.""" - - def test_bundle_declared_as_direct_git_reference(self) -> None: - """The bundle must be a PEP 508 direct git reference in [project.dependencies]. - - A direct `name @ git+https://...` reference survives `--no-sources`, - unlike a bare name (only resolvable from PyPI) or a [tool.uv.sources] entry. - """ - data = _load_pyproject() - deps: list[str] = data["project"]["dependencies"] - bundle_deps = [d for d in deps if _dep_name(d) == BUNDLE] - assert bundle_deps, f"Expected '{BUNDLE}' in dependencies, got: {deps}" - assert "git+https://" in bundle_deps[0], ( - f"Bundle dependency must be a direct git+https reference so it survives " - f"`uv pip install --no-sources`, got: {bundle_deps[0]!r}" - ) - - def test_bundle_is_not_a_uv_path_source(self) -> None: - """The bundle must NOT be a [tool.uv.sources] path entry. - - The `path = '../..'` assumption is exactly what breaks standalone install: - --no-sources strips [tool.uv.sources], leaving an unresolvable reference. - Two modules referencing the bundle via path while others use a git URL is - what produced uv's "conflicting URLs" abort on a single co-install command. - """ - data = _load_pyproject() - sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {}) - assert BUNDLE not in sources, ( - f"'{BUNDLE}' must not be a [tool.uv.sources] entry (breaks standalone " - f"install under --no-sources); declare it as a direct git reference in " - f"[project.dependencies] instead. Got sources: {sources}" - ) - - def test_dependencies_list_has_httpx_and_bundle(self) -> None: - """Production deps must include httpx and the bundle. - - amplifier-core is NOT a production dep — it is runtime-provided by the - Amplifier CLI. - """ - data = _load_pyproject() - deps: list[str] = data["project"]["dependencies"] - assert any("httpx" in d for d in deps), f"httpx not found in {deps}" - assert any(_dep_name(d) == BUNDLE for d in deps), f"{BUNDLE} not found in {deps}" - assert not any(_dep_name(d) == "amplifier-core" for d in deps), ( - f"amplifier-core must not be a production dep (runtime-provided): {deps}" - ) - - def test_allow_direct_references_enabled(self) -> None: - """Building a wheel that carries a direct reference requires this hatch flag.""" - data = _load_pyproject() - allow = ( - data.get("tool", {}).get("hatch", {}).get("metadata", {}).get("allow-direct-references") - ) - assert allow is True, ( - "tool.hatch.metadata.allow-direct-references must be true to build a wheel " - f"carrying the direct git reference, got: {allow!r}" - ) diff --git a/modules/tool-blob-read/uv.lock b/modules/tool-blob-read/uv.lock deleted file mode 100644 index b385fab..0000000 --- a/modules/tool-blob-read/uv.lock +++ /dev/null @@ -1,520 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "amplifier-bundle-context-intelligence" -version = "0.1.1" -source = { editable = "../../" } - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "httpx", specifier = ">=0.25" }, - { name = "idna", specifier = ">=3.15" }, - { name = "pyright", specifier = ">=1.1" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "ruff", specifier = ">=0.4" }, -] - -[[package]] -name = "amplifier-core" -version = "1.2.5" -source = { git = "https://github.com/microsoft/amplifier-core?branch=main#308b2455728378be896266cf620c00da2a408b65" } -dependencies = [ - { name = "click" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tomli" }, - { name = "typing-extensions" }, -] - -[[package]] -name = "amplifier-module-tool-blob-read" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "amplifier-bundle-context-intelligence" }, - { name = "httpx" }, - { name = "idna" }, -] - -[package.dev-dependencies] -dev = [ - { name = "amplifier-core" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "amplifier-bundle-context-intelligence", editable = "../../" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "idna", specifier = ">=3.15" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "amplifier-core", git = "https://github.com/microsoft/amplifier-core?branch=main" }, - { name = "pyright", specifier = ">=1.1" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "ruff", specifier = ">=0.4" }, -] - -[[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 = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "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 = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[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 = "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 = "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 = "idna" -version = "3.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[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 = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[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 = "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 = "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/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[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 = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - -[[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" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -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 = "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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - -[[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" }, -] diff --git a/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/__init__.py b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/__init__.py new file mode 100644 index 0000000..836682f --- /dev/null +++ b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/__init__.py @@ -0,0 +1,41 @@ +"""Context Intelligence read tools — graph_query and blob_read. + +Both tools share one ToolConfigResolver, so sources has a single +config namespace: overrides.tool-context-intelligence-query.config.sources. + +Two tools, one mount(): idiomatic multi-tool module (same as tool-filesystem +which mounts read_file / write_file / edit_file from one mount() call). +""" + +from __future__ import annotations + +from typing import Any + +__amplifier_module_type__ = "tool" + + +async def mount(coordinator: Any, config: Any) -> None: + """Mount both CI read tools, sharing one ToolConfigResolver. + + The resolver is built ONCE from the module's config and injected into + both tools. Tool constructors no longer accept config — the resolver IS + the config surface. + + The hook resolver is NOT fetched here; each tool fetches it lazily at + first execute() because tools mount before hooks (kernel phase order is + orchestrator → context → providers → tools → hooks — CONTRACTS.md §Module + Lifecycle Methods). Using on_session_ready() here would force cross-callback + instance references (multi-session anti-pattern), so lazy fetch is the + correct and intentional design. + """ + from context_intelligence.tool_resolver import ToolConfigResolver + + from .blob_read_tool import BlobReadTool + from .graph_query_tool import GraphQueryTool + + resolver = ToolConfigResolver(config or {}, coordinator) # built ONCE + gq = GraphQueryTool(coordinator, resolver) + br = BlobReadTool(coordinator, resolver) + await coordinator.mount("tools", gq, name=gq.name) # "graph_query" + await coordinator.mount("tools", br, name=br.name) # "blob_read" + return None # kernel ignores non-callable returns; resolver is pure → no cleanup diff --git a/modules/tool-blob-read/amplifier_module_tool_blob_read/blob_read_tool.py b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/blob_read_tool.py similarity index 73% rename from modules/tool-blob-read/amplifier_module_tool_blob_read/blob_read_tool.py rename to modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/blob_read_tool.py index d6fb9d1..9572684 100644 --- a/modules/tool-blob-read/amplifier_module_tool_blob_read/blob_read_tool.py +++ b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/blob_read_tool.py @@ -1,4 +1,17 @@ -"""BlobReadTool — fetches blob content from the context-intelligence server.""" +"""BlobReadTool — fetches blob content from the context-intelligence server. + +Configuration is resolved via the three-tier fallback chain in +``resolve_query_endpoint`` (same as GraphQueryTool — parity guaranteed by the +shared helper): + + 1. Explicit read-config (``sources:`` in mount config, if set). + 2. Upload destinations from ``context_intelligence.hook_config_resolver`` + capability (fixes the destinations-only config bug). + 3. ``AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL`` env var (canonical last-resort). + +The ``ToolConfigResolver`` is injected at construction time by ``mount()`` +(one shared instance for both CI read tools — single config namespace). +""" from __future__ import annotations @@ -8,7 +21,9 @@ from typing import Any from amplifier_core import ToolResult + from context_intelligence.client import AsyncCIClient +from context_intelligence.tool_resolver import ToolConfigResolver, resolve_query_endpoint _URI_SCHEME = "ci-blob://" _BLOB_DIR = Path("/tmp/ci-blobs") @@ -22,9 +37,10 @@ def _sanitize_path_component(s: str) -> str: class BlobReadTool: """Tool that fetches a ci-blob:// URI from the server and writes it to disk.""" - def __init__(self, coordinator: Any) -> None: + def __init__(self, coordinator: Any, resolver: ToolConfigResolver | None = None) -> None: self._coordinator = coordinator - self._resolver: Any = None + self._tool_resolver = resolver or ToolConfigResolver({}, coordinator) + self._hook_resolver: Any | None = None @property def name(self) -> str: @@ -51,22 +67,14 @@ def input_schema(self) -> dict[str, Any]: } async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 - # (1) Lazy capability resolution - if self._resolver is None: - self._resolver = self._coordinator.get_capability( - "context_intelligence.config_resolver" - ) - if self._resolver is None: - return ToolResult( - success=False, - error={ - "message": "context-intelligence hook not configured", - "type": "configuration_error", - }, + # (1) Lazy hook resolver resolution + if self._hook_resolver is None: + self._hook_resolver = self._coordinator.get_capability( + "context_intelligence.hook_config_resolver" ) - # (2) Get server_url from resolver - server_url: str | None = self._resolver.context_intelligence_server_url + # (2) Resolve server_url + api_key via three-tier chain + server_url, api_key = resolve_query_endpoint(self._hook_resolver, self._tool_resolver) if not server_url: return ToolResult( success=False, @@ -105,7 +113,6 @@ async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 safe_key = _sanitize_path_component(key) # (5) Construct AsyncCIClient - api_key: str | None = self._resolver.context_intelligence_api_key async_client = AsyncCIClient(server_url=server_url, api_key=api_key or "") # (6) Fetch blob using original unsanitized values for the server request diff --git a/modules/tool-graph-query/amplifier_module_tool_graph_query/graph_query_tool.py b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/graph_query_tool.py similarity index 64% rename from modules/tool-graph-query/amplifier_module_tool_graph_query/graph_query_tool.py rename to modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/graph_query_tool.py index 120f758..2016f38 100644 --- a/modules/tool-graph-query/amplifier_module_tool_graph_query/graph_query_tool.py +++ b/modules/tool-context-intelligence-query/amplifier_module_tool_context_intelligence_query/graph_query_tool.py @@ -1,8 +1,18 @@ """GraphQueryTool — agent-facing tool for executing Cypher queries. -Implements the Amplifier Tool protocol. Resolves configuration lazily -via the ``context_intelligence.config_resolver`` coordinator capability -registered by the hook-context-intelligence module. +Implements the Amplifier Tool protocol. Configuration is resolved via the +three-tier fallback chain in ``resolve_query_endpoint``: + + 1. Explicit read-config (``sources:`` in mount config, if set). + 2. Upload destinations from ``context_intelligence.hook_config_resolver`` + capability (fixes the destinations-only config bug). + 3. ``AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL`` env var (canonical last-resort). + +The hook resolver is fetched lazily at first ``execute()`` call so that late +mount order is handled correctly (tools mount before hooks). + +The ``ToolConfigResolver`` is injected at construction time by ``mount()`` +(one shared instance for both CI read tools — single config namespace). """ from __future__ import annotations @@ -10,6 +20,7 @@ from typing import Any from context_intelligence.client import AsyncCIClient +from context_intelligence.tool_resolver import ToolConfigResolver, resolve_query_endpoint from amplifier_core.models import ToolResult @@ -18,13 +29,14 @@ class GraphQueryTool: """Execute Cypher queries against the context-intelligence server. Implements the Amplifier Tool protocol (name, description, input_schema, - execute). Configuration is resolved lazily at execute() time via the - coordinator's ``context_intelligence.config_resolver`` capability. + execute). Configuration is resolved via resolve_query_endpoint() at + execute() time, consulting the hook's upload destinations as a fallback. """ - def __init__(self, coordinator: Any) -> None: + def __init__(self, coordinator: Any, resolver: ToolConfigResolver | None = None) -> None: self._coordinator = coordinator - self._resolver: Any | None = None + self._tool_resolver = resolver or ToolConfigResolver({}, coordinator) + self._hook_resolver: Any | None = None @property def name(self) -> str: @@ -74,22 +86,27 @@ def input_schema(self) -> dict[str, Any]: "required": ["query"], } - async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 - if self._resolver is None: - self._resolver = self._coordinator.get_capability( - "context_intelligence.config_resolver" - ) + def _resolve_server_config(self, coordinator: Any) -> tuple[str | None, str | None, str]: + """Resolve (server_url, api_key, workspace) using the three-tier fallback chain. - if self._resolver is None: - return ToolResult( - success=False, - error={ - "message": "context-intelligence hook not configured", - "type": "configuration_error", - }, + Late-mount upgrade: retries hook capability lookup on every call while + _hook_resolver is None (hook may mount after the tool). + """ + if self._hook_resolver is None: + self._hook_resolver = coordinator.get_capability( + "context_intelligence.hook_config_resolver" ) + url, api_key = resolve_query_endpoint(self._hook_resolver, self._tool_resolver) + workspace = ( + self._hook_resolver.workspace + if self._hook_resolver is not None + else self._tool_resolver.workspace + ) + return url, api_key, workspace + + async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 + server_url, api_key, workspace = self._resolve_server_config(self._coordinator) - server_url = self._resolver.context_intelligence_server_url if not server_url: return ToolResult( success=False, @@ -99,7 +116,6 @@ async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 }, ) - workspace = self._resolver.workspace query: str = input["query"] ws_override = input.get("workspace") effective_workspace = ws_override if ws_override is not None else workspace @@ -118,7 +134,6 @@ async def execute(self, input: dict[str, Any]) -> ToolResult: # noqa: A002 else: params = raw_params - api_key = self._resolver.context_intelligence_api_key async_client = AsyncCIClient(server_url=server_url, api_key=api_key or "") result = await async_client.cypher(query, effective_workspace, params=params) return ToolResult(success=True, output=result) diff --git a/modules/tool-graph-query/pyproject.toml b/modules/tool-context-intelligence-query/pyproject.toml similarity index 73% rename from modules/tool-graph-query/pyproject.toml rename to modules/tool-context-intelligence-query/pyproject.toml index 389f9b4..2aeb2bb 100644 --- a/modules/tool-graph-query/pyproject.toml +++ b/modules/tool-context-intelligence-query/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "amplifier-module-tool-graph-query" +name = "amplifier-module-tool-context-intelligence-query" version = "0.1.0" -description = "Graph query tool — executes Cypher queries against the context-intelligence server" +description = "CI read tools — graph_query (Cypher) and blob_read from the context-intelligence server" requires-python = ">=3.11" license = "MIT" @@ -12,7 +12,7 @@ dependencies = [ ] [project.entry-points."amplifier.modules"] -tool-graph-query = "amplifier_module_tool_graph_query:mount" +tool-context-intelligence-query = "amplifier_module_tool_context_intelligence_query:mount" [build-system] requires = ["hatchling"] @@ -22,7 +22,7 @@ build-backend = "hatchling.build" package = true [tool.hatch.build.targets.wheel] -packages = ["amplifier_module_tool_graph_query"] +packages = ["amplifier_module_tool_context_intelligence_query"] [tool.hatch.metadata] # Required to build a wheel that carries a PEP 508 direct-reference (git+https) dependency. @@ -46,6 +46,7 @@ pythonVersion = "3.11" typeCheckingMode = "basic" venvPath = "." venv = ".venv" +extraPaths = ["../.."] [tool.ruff] target-version = "py311" diff --git a/modules/tool-blob-read/tests/__init__.py b/modules/tool-context-intelligence-query/tests/__init__.py similarity index 100% rename from modules/tool-blob-read/tests/__init__.py rename to modules/tool-context-intelligence-query/tests/__init__.py diff --git a/modules/tool-context-intelligence-query/tests/test_blob_read_tool.py b/modules/tool-context-intelligence-query/tests/test_blob_read_tool.py new file mode 100644 index 0000000..7a27146 --- /dev/null +++ b/modules/tool-context-intelligence-query/tests/test_blob_read_tool.py @@ -0,0 +1,600 @@ +"""Tests for BlobReadTool — ported from tool-blob-read, updated for merged module. + +Constructor change: BlobReadTool(coordinator, resolver=None). +Tests that previously passed config= now inject a ToolConfigResolver directly. +Patch path updated to amplifier_module_tool_context_intelligence_query.blob_read_tool. +""" + +from __future__ import annotations + +import json +import os +import pathlib +import shutil +from contextlib import contextmanager +from types import SimpleNamespace +from typing import Any, Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from amplifier_core import ToolResult + +# --------------------------------------------------------------------------- +# Module-level helper functions +# --------------------------------------------------------------------------- + + +def _make_coordinator(resolver: Any) -> MagicMock: + """Return a MagicMock coordinator whose get_capability returns *resolver*.""" + coordinator = MagicMock() + coordinator.config = {} + coordinator.get_capability.return_value = resolver + return coordinator + + +def _make_hook_resolver(server_url: str | None, api_key: str | None = None) -> MagicMock: + """Return a MagicMock hook resolver (returned by get_capability). + + api_key defaults to None so tests that don't exercise auth get a clean mock. + Also sets destinations so _first_destination() can iterate it safely. + """ + resolver = MagicMock() + resolver.context_intelligence_server_url = server_url + resolver.context_intelligence_api_key = api_key + # destinations must be a real dict so _first_destination() can iterate it safely + if server_url: + resolver.destinations = { + "default": SimpleNamespace(name="default", url=server_url, api_key=api_key or ""), + } + else: + resolver.destinations = {} + return resolver + + +def _make_tool_resolver(config: dict = None) -> Any: + """Build a real ToolConfigResolver from config for injection.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + return ToolConfigResolver(config or {}, coord) + + +@contextmanager +def _patch_async_client( + fetch_blob_return: Any = None, +) -> Generator[tuple[MagicMock, MagicMock], None, None]: + """Context manager that patches AsyncCIClient in the blob_read_tool module. + + Yields ``(mock_cls, mock_instance)`` so callers can assert constructor + arguments (via ``mock_cls``) and ``fetch_blob`` call arguments (via + ``mock_instance``). + """ + mock_instance = MagicMock() + mock_instance.fetch_blob = AsyncMock(return_value=fetch_blob_return) + mock_cls = MagicMock(return_value=mock_instance) + with patch( + "amplifier_module_tool_context_intelligence_query.blob_read_tool.AsyncCIClient", + mock_cls, + ): + yield mock_cls, mock_instance + + +# --------------------------------------------------------------------------- +# Module-level fixture +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _clean_blob_dir(): # type: ignore[no-untyped-def] + """Remove /tmp/ci-blobs/ before each test to prevent cross-test pollution.""" + blob_dir = pathlib.Path("/tmp/ci-blobs") + if blob_dir.exists(): + shutil.rmtree(blob_dir) + yield + # Leave files after test for debugging; next test's setup cleans up + + +# --------------------------------------------------------------------------- +# (1) Protocol surface +# --------------------------------------------------------------------------- + + +class TestBlobReadToolProtocol: + """Tool protocol surface: name, description, schema, execute return type.""" + + def test_name_is_blob_read(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert tool.name == "blob_read" + + def test_description_mentions_ci_blob_scheme(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert "ci-blob://" in tool.description + + def test_description_mentions_file_path(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert "file path" in tool.description.lower() + assert "disk path" not in tool.description.lower() + + def test_schema_type_is_object(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert tool.input_schema["type"] == "object" + + def test_schema_has_uri_required(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert "uri" in tool.input_schema["required"] + + def test_schema_uri_is_string(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + assert tool.input_schema["properties"]["uri"]["type"] == "string" + + async def test_execute_returns_tool_result(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + tool = BlobReadTool(_make_coordinator(None)) + result = await tool.execute({"uri": "ci-blob://session/key"}) + assert isinstance(result, ToolResult) + + +# --------------------------------------------------------------------------- +# (2) Lazy capability resolution +# --------------------------------------------------------------------------- + + +class TestLazyCapabilityResolution: + """execute() must resolve the config capability lazily and cache it.""" + + async def test_capability_not_found_returns_configuration_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + # get_capability returns None → capability not registered + tool = BlobReadTool(_make_coordinator(None)) + # Clear CI env vars so tier-3 fallback does not accidentally succeed + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + result = await tool.execute({"uri": "ci-blob://session/key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + assert "not configured" in result.error["message"].lower() + + async def test_server_url_none_returns_configuration_error_with_url(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver(None) + tool = BlobReadTool(_make_coordinator(hook_resolver)) + # Clear CI env vars so tier-3 fallback does not accidentally succeed + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + result = await tool.execute({"uri": "ci-blob://session/key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + assert "url" in result.error["message"].lower() + + async def test_server_url_empty_returns_configuration_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + # Clear CI env vars so tier-3 fallback does not accidentally succeed + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + result = await tool.execute({"uri": "ci-blob://session/key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + + async def test_resolver_cached_after_first_lookup(self) -> None: + """get_capability should be called exactly once across two execute() calls.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + coordinator = _make_coordinator(hook_resolver) + tool = BlobReadTool(coordinator) + + with _patch_async_client(fetch_blob_return={"data": "first"}): + await tool.execute({"uri": "ci-blob://session/key1"}) + await tool.execute({"uri": "ci-blob://session/key2"}) + + coordinator.get_capability.assert_called_once() + + +# --------------------------------------------------------------------------- +# (3) URI parsing +# --------------------------------------------------------------------------- + + +class TestURIParsing: + """execute() must validate and parse ci-blob:// URIs before fetching.""" + + async def test_missing_scheme_returns_uri_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + # No "ci-blob://" prefix at all + result = await tool.execute({"uri": "session/key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "uri_error" + assert "ci-blob://" in result.error["message"] + + async def test_no_slash_after_scheme_returns_uri_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + # Scheme present but no "/" separating session_id from key + result = await tool.execute({"uri": "ci-blob://sessiononly"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "uri_error" + assert "session_id/key" in result.error["message"] + + async def test_valid_uri_extracts_session_and_key(self) -> None: + """A well-formed URI must call fetch_blob with correct session_id and key.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"ok": True}) as (_, mock_instance): + await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + mock_instance.fetch_blob.assert_called_once_with("my-session", "my-key") + + +# --------------------------------------------------------------------------- +# (4) Path sanitization +# --------------------------------------------------------------------------- + + +class TestPathSanitization: + """Output file paths must be confined to /tmp/ci-blobs/ regardless of key.""" + + async def test_slashes_sanitized(self) -> None: + """Slashes in the key must not create unexpected subdirectory depth.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"data": "test"}): + result = await tool.execute({"uri": "ci-blob://my-session/path/with/slashes"}) + + assert result.success is True + assert result.output is not None + output = str(result.output["path"]) + assert output.startswith("/tmp/ci-blobs/") + assert ".." not in output + + async def test_special_chars_sanitized(self) -> None: + """Path traversal sequences in the key must be neutralized in the output path.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"data": "test"}): + result = await tool.execute({"uri": "ci-blob://my-session/../../etc/passwd"}) + + assert result.success is True + assert result.output is not None + output = str(result.output["path"]) + assert output.startswith("/tmp/ci-blobs/") + + async def test_path_traversal_neutralized(self) -> None: + """Output path must always stay under /tmp/ci-blobs/ even with traversal key.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"data": "test"}): + result = await tool.execute({"uri": "ci-blob://my-session/../../etc/passwd"}) + + assert result.success is True + assert result.output is not None + output = str(result.output["path"]) + # Resolved path must be strictly under /tmp/ci-blobs/ + assert output.startswith("/tmp/ci-blobs/") + # Must not have escaped to /etc/ + assert "/etc/" not in output + + +# --------------------------------------------------------------------------- +# (5) Successful blob read +# --------------------------------------------------------------------------- + + +class TestBlobReadSuccess: + """Happy-path behaviour: file written to disk and path returned.""" + + async def test_successful_fetch_writes_file_and_returns_path(self) -> None: + """Dict blob must be written as json.dumps(data); output is the file path.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + blob_data = {"session": "test", "data": [1, 2, 3]} + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return=blob_data): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + assert result.output is not None + written = pathlib.Path(result.output["path"]) + assert written.exists() + assert json.loads(written.read_text()) == blob_data + + async def test_output_path_structure(self) -> None: + """Parent directory name == session_id, filename == key.json.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"ok": True}): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + assert result.output is not None + p = pathlib.Path(result.output["path"]) + assert p.parent.name == "my-session" + assert p.name == "my-key.json" + + async def test_string_blob_written_directly(self) -> None: + """String blob must be written as raw text, NOT wrapped in json.dumps.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + raw_string = "hello world, this is raw text" + + with _patch_async_client(fetch_blob_return=raw_string): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + assert result.output is not None + written = pathlib.Path(result.output["path"]) + assert written.exists() + # Must be written verbatim — not json.dumps'd (which would add quotes) + assert written.read_text() == raw_string + + +# --------------------------------------------------------------------------- +# (6) Error handling +# --------------------------------------------------------------------------- + + +class TestBlobReadErrors: + """execute() maps fetch_blob returning None to a typed http_error.""" + + async def test_fetch_blob_none_returns_http_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return=None): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "http_error" + + +# --------------------------------------------------------------------------- +# (7) Authorization / constructor arguments +# --------------------------------------------------------------------------- + + +class TestAuthHeader: + """AsyncCIClient must receive the correct api_key from the resolver.""" + + async def test_api_key_passed_to_async_ci_client(self) -> None: + """When api_key is set the AsyncCIClient must be constructed with it.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080", api_key="my-secret") + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): + await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + mock_cls.assert_called_once_with(server_url="http://localhost:8080", api_key="my-secret") + + async def test_none_api_key_passes_empty_string(self) -> None: + """When api_key is None the AsyncCIClient must receive an empty string.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver("http://localhost:8080", api_key=None) + tool = BlobReadTool(_make_coordinator(hook_resolver)) + + with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): + await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + mock_cls.assert_called_once_with(server_url="http://localhost:8080", api_key="") + + +# --------------------------------------------------------------------------- +# Helpers for config-fallback tests (§7 case #8: blob-read parity) +# --------------------------------------------------------------------------- + + +def _make_hook_resolver_br(destinations: dict) -> MagicMock: + """Hook resolver mock with specific destinations dict for blob-read tests.""" + resolver = MagicMock() + resolver.destinations = destinations + return resolver + + +def _make_dest_br(url: str, api_key: str) -> SimpleNamespace: + """Quick Destination-like SimpleNamespace for blob-read test doubles.""" + return SimpleNamespace(name="default", url=url, api_key=api_key) + + +def _tool_resolver_br(config: dict = None) -> Any: + """Build a ToolConfigResolver for blob-read config-fallback tests.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + return ToolConfigResolver(config or {}, coord) + + +# --------------------------------------------------------------------------- +# TestBlobReadConfigFallback — §7 case #8 parity tests +# --------------------------------------------------------------------------- + + +class TestBlobReadConfigFallback: + """Case #8: blob_read uses the same resolve_query_endpoint as graph_query (parity). + + Re-runs cases #1, #2, #5, #6 against BlobReadTool to prove identical resolution. + """ + + # Case #8/#1 — sources wins over destination + async def test_read_config_wins_over_destination(self) -> None: + """BlobReadTool case #8/#1: explicit sources wins over hook destinations.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver_br( + destinations={"default": _make_dest_br("http://upload.example.com", "upload-key")} + ) + coordinator = _make_coordinator(hook_resolver) + config = { + "sources": { + "default": {"url": "http://read.example.com", "api_key": "read-key"}, + } + } + resolver = _tool_resolver_br(config) + tool = BlobReadTool(coordinator, resolver) + + with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://read.example.com" + assert call_kwargs["api_key"] == "read-key" + + # Case #8/#2 — destinations-only falls through to tier 2 + async def test_destinations_only_falls_through_to_tier2(self) -> None: + """BlobReadTool case #8/#2: no sources key → uses hook destination.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver_br( + destinations={"default": _make_dest_br("http://dest.example.com", "dest-key")} + ) + coordinator = _make_coordinator(hook_resolver) + resolver = _tool_resolver_br({}) + tool = BlobReadTool(coordinator, resolver) + + with _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://dest.example.com" + assert call_kwargs["api_key"] == "dest-key" + + # Case #8/#5 — env hit + async def test_env_hit(self) -> None: + """BlobReadTool case #8/#5: canonical env vars work as tier-3 fallback.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver_br(destinations={}) + coordinator = _make_coordinator(hook_resolver) + resolver = _tool_resolver_br({}) + tool = BlobReadTool(coordinator, resolver) + + env_patch = { + "AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL": "http://env.example.com", + "AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY": "env-key", + } + with ( + patch.dict(os.environ, env_patch), + _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _), + ): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://env.example.com" + assert call_kwargs["api_key"] == "env-key" + + # Regression: env is below hook destination (tier 2 beats tier 3) + async def test_env_does_not_override_hook_destination(self) -> None: + """BlobReadTool regression: canonical env + hook destination → destination wins.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver_br( + destinations={"default": _make_dest_br("http://dest.example.com", "dest-key")} + ) + coordinator = _make_coordinator(hook_resolver) + resolver = _tool_resolver_br({}) + tool = BlobReadTool(coordinator, resolver) + + # Canonical env set — must NOT override the hook destination + env_patch = { + "AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL": "http://env-override.example.com", + "AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY": "env-override-key", + } + with ( + patch.dict(os.environ, env_patch), + _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _), + ): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Hook destination (tier 2) wins over env (tier 3) + assert call_kwargs["server_url"] == "http://dest.example.com" + assert call_kwargs["api_key"] == "dest-key" + + # Case #8/#6 — all miss → configuration_error + async def test_all_miss_returns_configuration_error(self) -> None: + """BlobReadTool case #8/#6: no config, no destinations, no env → configuration_error.""" + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + hook_resolver = _make_hook_resolver_br(destinations={}) + coordinator = _make_coordinator(hook_resolver) + resolver = _tool_resolver_br({}) + tool = BlobReadTool(coordinator, resolver) + + # Exclude ALL CI env vars (including canonical SERVER_URL / API_KEY) + clean_env = { + k: v + for k, v in os.environ.items() + if not k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_") + } + with ( + patch.dict(os.environ, clean_env, clear=True), + _patch_async_client(fetch_blob_return={"ok": True}) as (mock_cls, _), + ): + result = await tool.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" diff --git a/modules/tool-context-intelligence-query/tests/test_graph_query_tool.py b/modules/tool-context-intelligence-query/tests/test_graph_query_tool.py new file mode 100644 index 0000000..c7af25c --- /dev/null +++ b/modules/tool-context-intelligence-query/tests/test_graph_query_tool.py @@ -0,0 +1,842 @@ +"""Tests for GraphQueryTool — ported from tool-graph-query, updated for merged module. + +Constructor change: GraphQueryTool(coordinator, resolver=None). +Tests that previously passed config= now inject a ToolConfigResolver directly. +Patch path updated to amplifier_module_tool_context_intelligence_query.graph_query_tool. +""" + +from __future__ import annotations + +import os +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + + +def _make_coordinator(resolver: Any = None) -> MagicMock: + coordinator = MagicMock() + coordinator.config = {} + coordinator.get_capability = MagicMock(return_value=resolver) + return coordinator + + +def _make_hook_resolver( + server_url: str | None = "http://localhost:8080", + workspace: str = "test-workspace", + api_key: str | None = "test-api-key", +) -> MagicMock: + """Create a hook resolver mock (returned by get_capability).""" + resolver = MagicMock() + resolver.context_intelligence_server_url = server_url + resolver.workspace = workspace + resolver.context_intelligence_api_key = api_key + # destinations must be a real dict so _first_destination() can iterate it safely + if server_url: + resolver.destinations = { + "default": SimpleNamespace(name="default", url=server_url, api_key=api_key or ""), + } + else: + resolver.destinations = {} + return resolver + + +def _make_mock_async_ci_client(return_value: Any = None): + """Return (mock_instance, mock_cls) for patching AsyncCIClient.""" + mock_instance = AsyncMock() + mock_instance.cypher = AsyncMock(return_value=return_value if return_value is not None else []) + mock_cls = MagicMock(return_value=mock_instance) + return mock_instance, mock_cls + + +def _make_tool_resolver(config: dict) -> Any: + """Build a real ToolConfigResolver from a config dict (for injection).""" + from context_intelligence.tool_resolver import ToolConfigResolver + + return ToolConfigResolver(config, MagicMock()) + + +# --------------------------------------------------------------------------- +# TestGraphQueryToolProtocol +# --------------------------------------------------------------------------- + + +class TestGraphQueryToolProtocol: + """Tool protocol surface tests.""" + + def test_name_is_graph_query(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + assert tool.name == "graph_query" + + def test_description_mentions_cypher(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + assert "Cypher" in tool.description + + def test_description_mentions_context_intelligence(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + assert "context-intelligence" in tool.description + + def test_input_schema_returns_object_type(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + assert tool.input_schema["type"] == "object" + + def test_input_schema_has_query_as_required(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + assert "query" in tool.input_schema["required"] + + def test_input_schema_has_optional_params_and_workspace(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + tool = GraphQueryTool(_make_coordinator()) + props = tool.input_schema["properties"] + assert "params" in props + assert "workspace" in props + # Neither should be required + assert "params" not in tool.input_schema["required"] + assert "workspace" not in tool.input_schema["required"] + + async def test_execute_returns_tool_result(self) -> None: + from amplifier_core.models import ToolResult + + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n LIMIT 1"}) + + assert isinstance(result, ToolResult) + + +# --------------------------------------------------------------------------- +# TestLazyCapabilityResolution +# --------------------------------------------------------------------------- + + +class TestLazyCapabilityResolution: + """Lazy resolver lookup and caching behaviour.""" + + async def test_capability_not_found_returns_configuration_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + coordinator = _make_coordinator(resolver=None) + tool = GraphQueryTool(coordinator) + # Clear all CI env vars so tier-3 fallback does not accidentally succeed + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + + async def test_server_url_none_returns_configuration_error(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(server_url=None) + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + # Clear all CI env vars so tier-3 fallback does not accidentally succeed + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + + async def test_resolver_cached_after_first_lookup(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n LIMIT 1"}) + await tool.execute({"query": "MATCH (n) RETURN n LIMIT 2"}) + + # get_capability should only be called once (on first execute) + coordinator.get_capability.assert_called_once_with( + "context_intelligence.hook_config_resolver" + ) + + async def test_configured_resolver_succeeds(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + + +# --------------------------------------------------------------------------- +# TestGraphQuery +# --------------------------------------------------------------------------- + + +class TestGraphQuery: + """AsyncCIClient construction and delegation tests.""" + + async def test_client_constructed_with_server_url_and_api_key(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(server_url="http://ci-server:9000", api_key="my-key") + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n"}) + + mock_cls.assert_called_once() + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs.get("server_url") == "http://ci-server:9000" + assert call_kwargs.get("api_key") == "my-key" + + async def test_workspace_injected_into_cypher_call(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(workspace="my-workspace") + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n"}) + + cypher_args = mock_instance.cypher.call_args + assert cypher_args is not None + # workspace is the 2nd positional arg: cypher(query, workspace) + all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) + assert "my-workspace" in all_args + + async def test_result_forwarded_from_cypher_call(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + expected = [{"n": {"id": "session-1"}}] + mock_instance, mock_cls = _make_mock_async_ci_client(return_value=expected) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n:Session) RETURN n LIMIT 10"}) + + assert result.success is True + assert result.output == expected + + +# --------------------------------------------------------------------------- +# TestGraphQueryWorkspaceOverride +# --------------------------------------------------------------------------- + + +class TestGraphQueryWorkspaceOverride: + """Per-call workspace override behaviour.""" + + async def test_per_call_workspace_override(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(workspace="default-workspace") + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n", "workspace": "override-workspace"}) + + cypher_args = mock_instance.cypher.call_args + all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) + assert "override-workspace" in all_args + + async def test_wildcard_workspace_override(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(workspace="default-workspace") + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n", "workspace": "*"}) + + cypher_args = mock_instance.cypher.call_args + all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) + assert "*" in all_args + + async def test_default_workspace_from_resolver(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(workspace="resolver-workspace") + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + # No workspace key in input — should fall back to resolver's workspace + await tool.execute({"query": "MATCH (n) RETURN n"}) + + cypher_args = mock_instance.cypher.call_args + all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) + assert "resolver-workspace" in all_args + + +# --------------------------------------------------------------------------- +# TestGraphQueryErrors +# --------------------------------------------------------------------------- + + +class TestGraphQueryErrors: + """Error path tests — AsyncCIClient.cypher() returns [] on HTTP failure (graceful degradation).""" + + async def test_server_error_returns_success_with_empty_result(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + # AsyncCIClient.cypher() returns [] on HTTP error (graceful degradation) + mock_instance, mock_cls = _make_mock_async_ci_client(return_value=[]) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + assert result.output == [] + + async def test_none_api_key_passed_as_empty_string(self) -> None: + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver(api_key=None) + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + # Should succeed and pass empty string as api_key + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs.get("api_key") == "" + + +# --------------------------------------------------------------------------- +# TestGraphQueryParamsForwarding — regression for params wiring bug +# --------------------------------------------------------------------------- + + +class TestGraphQueryParamsForwarding: + """Regression: user-supplied params reach AsyncCIClient.cypher().""" + + async def test_params_are_forwarded_to_async_client_cypher(self) -> None: + """params={...} from tool input must be forwarded as a kwarg to cypher().""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute( + { + "query": "MATCH (s:Session {id: $session_id}) RETURN s", + "params": {"session_id": "abc"}, + } + ) + + assert result.success is True + cypher_call = mock_instance.cypher.call_args + assert cypher_call is not None + # params must arrive as the 'params' keyword argument + assert cypher_call.kwargs.get("params") == {"session_id": "abc"} + + async def test_none_params_sends_empty_dict_to_cypher(self) -> None: + """Omitting params from tool input must default to {} at cypher().""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + mock_instance, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + await tool.execute({"query": "MATCH (n) RETURN n"}) + + cypher_call = mock_instance.cypher.call_args + assert cypher_call is not None + assert cypher_call.kwargs.get("params") == {} + + async def test_non_dict_params_returns_validation_error(self) -> None: + """Passing params as a non-dict must return a validation_error ToolResult.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + hook_resolver = _make_hook_resolver() + coordinator = _make_coordinator(resolver=hook_resolver) + tool = GraphQueryTool(coordinator) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n", "params": "not-a-dict"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "validation_error" + + +# --------------------------------------------------------------------------- +# Helpers for config-fallback tests (§7 matrix) +# --------------------------------------------------------------------------- + + +def _make_dest(url: str, api_key: str) -> SimpleNamespace: + """Quick Destination-like SimpleNamespace for test doubles.""" + name = "default" + return SimpleNamespace(name=name, url=url, api_key=api_key) + + +def _make_hook_resolver_with_dests(destinations: dict) -> MagicMock: + """Hook resolver mock with specific destinations dict.""" + resolver = MagicMock() + resolver.workspace = "test-workspace" + resolver.destinations = destinations + return resolver + + +def _tool_resolver_with_config(config: dict, coordinator: Any = None) -> Any: + """Build a real ToolConfigResolver from config for injection.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = coordinator or MagicMock() + coord.config = {} + return ToolConfigResolver(config, coord) + + +# --------------------------------------------------------------------------- +# TestConfigFallback — §7 test matrix cases #1–#7/#9–#10 +# --------------------------------------------------------------------------- + + +class TestConfigFallback: + """Config-resolution three-tier fallback (spec §7). + + Tests the explicit-read-config-first, then upload-destination, then env + precedence order — the core bug fix and its coherent read-config model. + """ + + # --- Case #1 / #4: sources wins over hook destination --- + + async def test_case1_read_config_wins_over_destination(self) -> None: + """Case #1: explicit sources wins over upload destination (tier 1 > tier 2).""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://upload.example.com", "upload-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + config = { + "sources": { + "default": {"url": "http://read.example.com", "api_key": "read-key"}, + } + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://read.example.com" + assert call_kwargs["api_key"] == "read-key" + + # --- Case #2: CORE BUG FIX — destinations-only config succeeds --- + + async def test_case2_destinations_only_falls_through_to_tier2(self) -> None: + """Case #2: no sources key + no legacy scalar → falls through to hook destinations.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://dest.example.com", "dest-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + # Tool resolver with empty config — no sources key, no legacy scalars + resolver = _tool_resolver_with_config({}) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://dest.example.com" + assert call_kwargs["api_key"] == "dest-key" + + # --- Case #3: per-field independence --- + + async def test_case3_read_config_url_empty_falls_to_destination_for_url(self) -> None: + """Case #3: sources url="" → url falls to destination; api_key stays at tier 1.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://dest.example.com", "dest-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + config = { + "sources": { + "default": {"url": "", "api_key": "read-key"}, # url is empty + } + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # url falls through from tier 1 (empty) to tier 2 (destination) + assert call_kwargs["server_url"] == "http://dest.example.com" + # api_key stays at tier 1 + assert call_kwargs["api_key"] == "read-key" + + # --- Case #4: explicit-first precedence assertion --- + + async def test_case4_explicit_read_config_wins_both_fields(self) -> None: + """Case #4: BOTH url and api_key come from tier 1 when sources is set.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://upload.example.com", "upload-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + config = { + "sources": { + "default": {"url": "http://read.example.com", "api_key": "read-key"}, + } + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Both fields must come from tier 1 (explicit read config) + assert call_kwargs["server_url"] == "http://read.example.com" + assert call_kwargs["api_key"] == "read-key" + + # --- Case #5: env hit --- + + async def test_case5_env_hit_when_no_config_or_destinations(self) -> None: + """Case #5: canonical env vars work as tier-3 fallback (below hook destination).""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests(destinations={}) # no destinations + coordinator = _make_coordinator(resolver=dest_resolver) + resolver = _tool_resolver_with_config({}) + tool = GraphQueryTool(coordinator, resolver) + + env_patch = { + "AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL": "http://env.example.com", + "AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY": "env-key", + } + _, mock_cls = _make_mock_async_ci_client() + with ( + patch.dict(os.environ, env_patch), + patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ), + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://env.example.com" + assert call_kwargs["api_key"] == "env-key" + + # --- Regression: env is below hook destination (tier 2 beats tier 3) --- + + async def test_case5b_env_does_not_override_hook_destination(self) -> None: + """Regression: canonical env vars set + hook destination present → destination wins. + + Locks in that env (tier 3) never outranks the hook destination (tier 2). + """ + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://dest.example.com", "dest-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + resolver = _tool_resolver_with_config({}) + tool = GraphQueryTool(coordinator, resolver) + + # Canonical env set — must NOT override the hook destination + env_patch = { + "AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL": "http://env-override.example.com", + "AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY": "env-override-key", + } + _, mock_cls = _make_mock_async_ci_client() + with ( + patch.dict(os.environ, env_patch), + patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ), + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Hook destination (tier 2) wins over env (tier 3) + assert call_kwargs["server_url"] == "http://dest.example.com" + assert call_kwargs["api_key"] == "dest-key" + + # --- Case #6: all miss → configuration_error --- + + async def test_case6_all_miss_returns_configuration_error(self) -> None: + """Case #6: no sources key, no destinations, no env → configuration_error.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests(destinations={}) + coordinator = _make_coordinator(resolver=dest_resolver) + resolver = _tool_resolver_with_config({}) + tool = GraphQueryTool(coordinator, resolver) + + # Exclude ALL CI env vars (including canonical SERVER_URL / API_KEY) + clean_env = { + k: v + for k, v in os.environ.items() + if not k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_") + } + _, mock_cls = _make_mock_async_ci_client() + with ( + patch.dict(os.environ, clean_env, clear=True), + patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ), + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + + # --- Case #7: multi-entry ordering determinism --- + + async def test_case7_multi_entry_ordering_first_read_entry_wins(self) -> None: + """Case #7: first entry in sources (insertion order) wins.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={ + "d1": SimpleNamespace(name="d1", url="http://d1.example.com", api_key="d1-key"), + "d2": SimpleNamespace(name="d2", url="http://d2.example.com", api_key="d2-key"), + } + ) + coordinator = _make_coordinator(resolver=dest_resolver) + config = { + "sources": { + "alpha": {"url": "http://alpha.example.com", "api_key": "alpha-key"}, + "beta": {"url": "http://beta.example.com", "api_key": "beta-key"}, + } + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # First sources entry ("alpha") wins + assert call_kwargs["server_url"] == "http://alpha.example.com" + assert call_kwargs["api_key"] == "alpha-key" + + async def test_case7_second_execute_same_result_deterministic(self) -> None: + """Case #7: repeated executes give the same endpoint (deterministic order).""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={ + "d1": SimpleNamespace(name="d1", url="http://d1.example.com", api_key="d1") + } + ) + coordinator = _make_coordinator(resolver=dest_resolver) + config = { + "sources": { + "alpha": {"url": "http://alpha.example.com", "api_key": "alpha-key"}, + } + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + r1 = await tool.execute({"query": "MATCH (n) RETURN n"}) + r2 = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert r1.success is True + assert r2.success is True + # Both executes used the same endpoint + calls = mock_cls.call_args_list + assert ( + calls[0].kwargs["server_url"] + == calls[1].kwargs["server_url"] + == "http://alpha.example.com" + ) + + # --- Case #9: legacy top-level scalar synthesizes default --- + + async def test_case9_legacy_scalars_synthesize_read_default_wins_tier1(self) -> None: + """Case #9: legacy context_intelligence_server_url+api_key synthesize sources.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://upload.example.com", "upload-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + # Legacy scalars in tool config — no sources key + config = { + "context_intelligence_server_url": "http://legacy.example.com", + "context_intelligence_api_key": "legacy-key", + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Synthesized default ("http://legacy.example.com") wins at tier 1 + assert call_kwargs["server_url"] == "http://legacy.example.com" + assert call_kwargs["api_key"] == "legacy-key" + + # --- Case #10: legacy url-only → no synthesis, falls through --- + + async def test_case10_legacy_url_only_no_synthesis_falls_through_to_destination(self) -> None: + """Case #10: legacy url-only (no api_key) → sources={}, falls to tier 2.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + dest_resolver = _make_hook_resolver_with_dests( + destinations={"default": _make_dest("http://upload.example.com", "upload-key")} + ) + coordinator = _make_coordinator(resolver=dest_resolver) + # Only server_url, no api_key → both-fields-required not met → no synthesis + config = { + "context_intelligence_server_url": "http://legacy.example.com", + # no context_intelligence_api_key + } + resolver = _tool_resolver_with_config(config) + tool = GraphQueryTool(coordinator, resolver) + + _, mock_cls = _make_mock_async_ci_client() + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # No synthesis → sources={} → falls through to tier 2 + assert call_kwargs["server_url"] == "http://upload.example.com" + assert call_kwargs["api_key"] == "upload-key" diff --git a/modules/tool-context-intelligence-query/tests/test_module.py b/modules/tool-context-intelligence-query/tests/test_module.py new file mode 100644 index 0000000..a5a32a8 --- /dev/null +++ b/modules/tool-context-intelligence-query/tests/test_module.py @@ -0,0 +1,519 @@ +"""Module-level contract tests for tool-context-intelligence-query. + +Tests for the merged two-tool module: mount registers both tools from one call, +the ToolConfigResolver is shared (one instance, identical resolution), the lazy +hook lookup stays lazy (not cached at mount time), and malformed/empty destination +inputs fail loud or fall through correctly. +""" + +from __future__ import annotations + +import asyncio +import inspect +import os +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_coordinator(hook_resolver: Any = None) -> MagicMock: + """Coordinator whose get_capability returns hook_resolver.""" + coordinator = MagicMock() + coordinator.config = {} + coordinator.get_capability = MagicMock(return_value=hook_resolver) + coordinator.mount = AsyncMock() + return coordinator + + +def _make_dest(url: str, api_key: str) -> SimpleNamespace: + return SimpleNamespace(name="default", url=url, api_key=api_key) + + +def _make_hook_resolver(url: str | None = None, api_key: str | None = None) -> MagicMock: + """Minimal hook resolver mock with a destinations dict.""" + resolver = MagicMock() + resolver.workspace = "test-workspace" + if url: + resolver.destinations = {"default": _make_dest(url, api_key or "")} + else: + resolver.destinations = {} + return resolver + + +def _graph_tool(coordinator: Any, resolver: Any = None) -> Any: + from context_intelligence.tool_resolver import ToolConfigResolver + + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + + if resolver is None: + resolver = ToolConfigResolver({}, coordinator) + return GraphQueryTool(coordinator, resolver) + + +def _blob_tool(coordinator: Any, resolver: Any = None) -> Any: + from context_intelligence.tool_resolver import ToolConfigResolver + + from amplifier_module_tool_context_intelligence_query.blob_read_tool import BlobReadTool + + if resolver is None: + resolver = ToolConfigResolver({}, coordinator) + return BlobReadTool(coordinator, resolver) + + +# --------------------------------------------------------------------------- +# TestModuleContract +# --------------------------------------------------------------------------- + + +class TestModuleContract: + """Module-level contract (type marker + mount signature).""" + + def test_module_type_is_tool(self) -> None: + from amplifier_module_tool_context_intelligence_query import __amplifier_module_type__ + + assert __amplifier_module_type__ == "tool" + + def test_mount_is_coroutine(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + assert inspect.iscoroutinefunction(mount) + + def test_mount_signature_has_coordinator_and_config(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + sig = inspect.signature(mount) + params = list(sig.parameters.keys()) + assert params[0] == "coordinator" + assert params[1] == "config" + + +# --------------------------------------------------------------------------- +# TestMountRegistersExactlyTwoTools +# --------------------------------------------------------------------------- + + +class TestMountRegistersExactlyTwoTools: + """mount() must register exactly two tools with distinct names.""" + + async def test_mount_registers_exactly_two_tools(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + await mount(coordinator, config={}) + + assert coordinator.mount.call_count == 2 + + async def test_both_tool_calls_use_tools_category(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + await mount(coordinator, config={}) + + for call in coordinator.mount.call_args_list: + assert call.args[0] == "tools" + + async def test_tool_names_are_graph_query_and_blob_read(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + await mount(coordinator, config={}) + + registered_names = {call.kwargs["name"] for call in coordinator.mount.call_args_list} + assert registered_names == {"graph_query", "blob_read"} + + async def test_mounted_tools_are_protocol_compliant(self) -> None: + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + await mount(coordinator, config={}) + + for call in coordinator.mount.call_args_list: + tool = call.args[1] + assert hasattr(tool, "name") + assert hasattr(tool, "description") + assert hasattr(tool, "input_schema") + assert hasattr(tool, "execute") + assert isinstance(tool.input_schema, dict) + assert inspect.iscoroutinefunction(tool.execute) + + async def test_mount_returns_none(self) -> None: + """mount() returns None — the kernel ignores non-callable returns.""" + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + result = await mount(coordinator, config={}) + assert result is None + + +# --------------------------------------------------------------------------- +# TestSharedResolverInvariant +# --------------------------------------------------------------------------- + + +class TestSharedResolverInvariant: + """The ToolConfigResolver is shared: one instance, identical resolution.""" + + async def test_both_tools_have_same_resolver_instance(self) -> None: + """gq._tool_resolver is br._tool_resolver: same object from mount().""" + from amplifier_module_tool_context_intelligence_query import mount + + coordinator = _make_coordinator() + await mount(coordinator, config={}) + + tools = {call.kwargs["name"]: call.args[1] for call in coordinator.mount.call_args_list} + gq = tools["graph_query"] + br = tools["blob_read"] + assert gq._tool_resolver is br._tool_resolver + + async def test_shared_resolver_consistency_same_url_and_api_key(self) -> None: + """Both tools resolve to the SAME (url, api_key) from sources. + + This is the load-bearing correctness invariant: with a shared resolver, + divergent read-endpoint config is structurally impossible. + """ + from amplifier_module_tool_context_intelligence_query import mount + from context_intelligence.tool_resolver import resolve_query_endpoint + + config = { + "sources": { + "primary": {"url": "http://read.example.com", "api_key": "shared-key"}, + } + } + coordinator = _make_coordinator() + await mount(coordinator, config=config) + + tools = {call.kwargs["name"]: call.args[1] for call in coordinator.mount.call_args_list} + gq = tools["graph_query"] + br = tools["blob_read"] + + # Resolve using the shared resolver (no hook resolver needed for tier-1 hit) + gq_url, gq_key = resolve_query_endpoint(None, gq._tool_resolver) + br_url, br_key = resolve_query_endpoint(None, br._tool_resolver) + + assert gq_url == br_url == "http://read.example.com" + assert gq_key == br_key == "shared-key" + + async def test_concurrent_resolution_is_consistent(self) -> None: + """Execute both tools 'concurrently'; both resolve to the same endpoint. + + ToolConfigResolver.sources is synchronous (no await between + cache-check and cache-set), so under asyncio there is no interleaving. + This test confirms identical results when both tools run concurrently. + """ + from unittest.mock import AsyncMock as AM + + from amplifier_module_tool_context_intelligence_query import mount + + config = { + "sources": { + "primary": {"url": "http://shared.example.com", "api_key": "shared-key"}, + } + } + hook_resolver = _make_hook_resolver(url="http://hook.example.com", api_key="hook-key") + coordinator = _make_coordinator(hook_resolver=hook_resolver) + await mount(coordinator, config=config) + + tools = {call.kwargs["name"]: call.args[1] for call in coordinator.mount.call_args_list} + gq = tools["graph_query"] + br = tools["blob_read"] + + # Mock the AsyncCIClient for both tools + gq_client = MagicMock() + gq_client.cypher = AM(return_value=[]) + br_client = MagicMock() + br_client.fetch_blob = AM(return_value=None) # None → http_error, but url is resolved + + gq_results = [] + br_results = [] + + with ( + patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + return_value=gq_client, + ) as gq_cls, + patch( + "amplifier_module_tool_context_intelligence_query.blob_read_tool.AsyncCIClient", + return_value=br_client, + ) as br_cls, + ): + gq_task = asyncio.create_task(gq.execute({"query": "MATCH (n) RETURN n"})) + br_task = asyncio.create_task(br.execute({"uri": "ci-blob://s/k"})) + gq_result, br_result = await asyncio.gather(gq_task, br_task) + gq_results.append(gq_cls.call_args) + br_results.append(br_cls.call_args) + + # Both tools must resolve to the shared read-destinations URL (tier 1) + assert gq_results[0] is not None + assert br_results[0] is not None + # AsyncCIClient is always called with keyword args (server_url=, api_key=) + gq_url = gq_results[0].kwargs.get("server_url") + br_url = br_results[0].kwargs.get("server_url") + # Read-destinations (tier 1) wins over hook destination (tier 2) + assert gq_url == "http://shared.example.com" + assert br_url == "http://shared.example.com" + + +# --------------------------------------------------------------------------- +# TestLateMount_TimingInvariant +# --------------------------------------------------------------------------- + + +class TestLateMountTimingInvariant: + """The lazy hook-resolver lookup must NOT be cached at mount() time. + + Catches any regression where the hook capability is fetched eagerly in mount() + rather than lazily in execute() (which would break when the hook mounts later). + """ + + async def test_late_mount_graph_query_resolves_destination_after_hook_registers( + self, + ) -> None: + """Mount with NO hook → register hook AFTER → execute() sees the hook's destination.""" + from amplifier_module_tool_context_intelligence_query import mount + + # Step 1: mount with no hook registered + coordinator = _make_coordinator(hook_resolver=None) + await mount(coordinator, config={}) + tools = {call.kwargs["name"]: call.args[1] for call in coordinator.mount.call_args_list} + gq = tools["graph_query"] + + # Confirm hook resolver is None after mount (lazy, not fetched yet) + assert gq._hook_resolver is None + + # Step 2: register the hook resolver AFTER mount + hook_resolver = _make_hook_resolver(url="http://late-hook.example.com", api_key="late-key") + coordinator.get_capability.return_value = hook_resolver + + # Step 3: execute() must now see the late-registered hook destination + mock_client = MagicMock() + mock_client.cypher = AsyncMock(return_value=[]) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await gq.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Tier 2 (hook destination) resolved because no sources key in config + assert call_kwargs["server_url"] == "http://late-hook.example.com" + assert call_kwargs["api_key"] == "late-key" + + async def test_late_mount_blob_read_resolves_destination_after_hook_registers( + self, + ) -> None: + """BlobReadTool: mount with no hook → register hook → execute sees destination.""" + import pathlib + import shutil + + from amplifier_module_tool_context_intelligence_query import mount + + # Cleanup blob dir + blob_dir = pathlib.Path("/tmp/ci-blobs") + if blob_dir.exists(): + shutil.rmtree(blob_dir) + + # Step 1: mount with no hook + coordinator = _make_coordinator(hook_resolver=None) + await mount(coordinator, config={}) + tools = {call.kwargs["name"]: call.args[1] for call in coordinator.mount.call_args_list} + br = tools["blob_read"] + + assert br._hook_resolver is None + + # Step 2: register hook AFTER mount + hook_resolver = _make_hook_resolver(url="http://late-hook.example.com", api_key="late-key") + coordinator.get_capability.return_value = hook_resolver + + # Step 3: execute resolves from the hook destination + mock_client = MagicMock() + mock_client.fetch_blob = AsyncMock(return_value={"ok": True}) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.blob_read_tool.AsyncCIClient", + mock_cls, + ): + result = await br.execute({"uri": "ci-blob://my-session/my-key"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://late-hook.example.com" + assert call_kwargs["api_key"] == "late-key" + + +# --------------------------------------------------------------------------- +# TestMalformedDestinationInputs +# --------------------------------------------------------------------------- + + +class TestMalformedDestinationInputs: + """Malformed / empty destination inputs must fail loud or fall through correctly.""" + + async def test_empty_sources_list_falls_through(self) -> None: + """sources: [] (a list, not dict) — _first_entry() returns None → falls to tier 2.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + # sources as a list (malformed — _first_entry guards isinstance(mapping, dict)) + config = {"sources": []} + resolver = ToolConfigResolver(config, coord) + + hook_resolver = _make_hook_resolver( + url="http://fallback.example.com", api_key="fallback-key" + ) + coordinator = _make_coordinator(hook_resolver=hook_resolver) + tool = GraphQueryTool(coordinator, resolver) + + mock_client = MagicMock() + mock_client.cypher = AsyncMock(return_value=[]) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # Falls through to tier 2 (hook destination) + assert call_kwargs["server_url"] == "http://fallback.example.com" + + async def test_empty_sources_dict_falls_through(self) -> None: + """sources: {} (empty dict) → first entry is None → falls to tier 2.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + config = {"sources": {}} + resolver = ToolConfigResolver(config, coord) + + hook_resolver = _make_hook_resolver( + url="http://fallback.example.com", api_key="fallback-key" + ) + coordinator = _make_coordinator(hook_resolver=hook_resolver) + tool = GraphQueryTool(coordinator, resolver) + + mock_client = MagicMock() + mock_client.cypher = AsyncMock(return_value=[]) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://fallback.example.com" + + async def test_entry_with_empty_url_falls_through_url_field(self) -> None: + """Entry with url: '' → url is falsy → _pick skips it → tier 2 wins for url.""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + config = { + "sources": { + "primary": {"url": "", "api_key": "read-key"}, + } + } + resolver = ToolConfigResolver(config, coord) + + hook_resolver = _make_hook_resolver(url="http://fallback.example.com", api_key="hook-key") + coordinator = _make_coordinator(hook_resolver=hook_resolver) + tool = GraphQueryTool(coordinator, resolver) + + mock_client = MagicMock() + mock_client.cypher = AsyncMock(return_value=[]) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + # url is empty in tier 1 → falls through to tier 2 (hook destination) + assert call_kwargs["server_url"] == "http://fallback.example.com" + # api_key stays at tier 1 (non-empty "read-key") + assert call_kwargs["api_key"] == "read-key" + + async def test_all_tiers_miss_returns_loud_configuration_error(self) -> None: + """No config, no destinations, no env → configuration_error (loud, not silent empty).""" + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + resolver = ToolConfigResolver({}, coord) + + hook_resolver = _make_hook_resolver(url=None) # empty destinations + coordinator = _make_coordinator(hook_resolver=hook_resolver) + tool = GraphQueryTool(coordinator, resolver) + + clean_env = { + k: v + for k, v in os.environ.items() + if not k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_") + } + mock_client = MagicMock() + mock_cls = MagicMock(return_value=mock_client) + with ( + patch.dict(os.environ, clean_env, clear=True), + patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ), + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + assert result.success is False + assert result.error is not None + assert result.error["type"] == "configuration_error" + # Must not have called the client (fails before reaching that point) + mock_cls.assert_not_called() + + async def test_none_sources_treated_as_absent(self) -> None: + """If sources is explicitly None/null → absent-key semantics (legacy synthesis). + + The ToolConfigResolver treats None as a non-dict → falls to legacy synthesis. + With no legacy scalars either, result is {} → falls through. + """ + from amplifier_module_tool_context_intelligence_query.graph_query_tool import GraphQueryTool + from context_intelligence.tool_resolver import ToolConfigResolver + + coord = MagicMock() + coord.config = {} + # sources: null — the sentinel detection in ToolConfigResolver checks + # 'raw is not _sentinel' → key IS present (None). isinstance(None, dict) is False → {} + config = {"sources": None} + resolver = ToolConfigResolver(config, coord) + + hook_resolver = _make_hook_resolver(url="http://hook.example.com", api_key="hook-key") + coordinator = _make_coordinator(hook_resolver=hook_resolver) + tool = GraphQueryTool(coordinator, resolver) + + mock_client = MagicMock() + mock_client.cypher = AsyncMock(return_value=[]) + mock_cls = MagicMock(return_value=mock_client) + with patch( + "amplifier_module_tool_context_intelligence_query.graph_query_tool.AsyncCIClient", + mock_cls, + ): + result = await tool.execute({"query": "MATCH (n) RETURN n"}) + + # Nsources → {} → falls to tier 2 (hook destination) + assert result.success is True + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["server_url"] == "http://hook.example.com" diff --git a/modules/tool-graph-query/tests/test_tool_dependencies.py b/modules/tool-context-intelligence-query/tests/test_tool_dependencies.py similarity index 64% rename from modules/tool-graph-query/tests/test_tool_dependencies.py rename to modules/tool-context-intelligence-query/tests/test_tool_dependencies.py index f8952e6..abd543c 100644 --- a/modules/tool-graph-query/tests/test_tool_dependencies.py +++ b/modules/tool-context-intelligence-query/tests/test_tool_dependencies.py @@ -1,15 +1,14 @@ -"""Test that the graph-query tool pyproject.toml declares the bundle dependency in a -form that installs standalone (outside the monorepo). - -The tool imports from the `context_intelligence` package shipped by the parent -bundle. For the tool to install standalone under the Amplifier agent's -`uv pip install --no-sources` policy, the bundle MUST be referenced as a PEP 508 -direct git reference inside [project.dependencies] (which survives --no-sources), -NOT via a [tool.uv.sources] `path = "../.."` entry (which --no-sources strips). - -This guard mirrors hook-context-intelligence/tests/test_hook_dependencies.py so -that every module references the parent bundle uniformly and cannot regress to a -uv path source (the original cause of the uv conflicting-URLs co-install failure). +"""Test that tool-context-intelligence-query pyproject.toml declares the bundle dependency +in a form that installs standalone (outside the monorepo). + +The tools import from the `context_intelligence` package shipped by the parent bundle. +For the module to install standalone under the Amplifier agent's `uv pip install +--no-sources` policy, the bundle MUST be referenced as a PEP 508 direct git reference +inside [project.dependencies] (which survives --no-sources), NOT via a +[tool.uv.sources] `path = "../.."` entry (which --no-sources strips). + +Merged from tool-graph-query and tool-blob-read versions (both were identical except +for module name) — now a single guard for the merged module. """ from __future__ import annotations @@ -37,7 +36,7 @@ def _dep_name(dep: str) -> str: class TestToolDependencies: - """Verify the tool declares the bundle as a standalone-installable dependency.""" + """Verify the module declares the bundle as a standalone-installable dependency.""" def test_bundle_declared_as_direct_git_reference(self) -> None: """The bundle must be a PEP 508 direct git reference in [project.dependencies]. @@ -59,8 +58,6 @@ def test_bundle_is_not_a_uv_path_source(self) -> None: The `path = '../..'` assumption is exactly what breaks standalone install: --no-sources strips [tool.uv.sources], leaving an unresolvable reference. - Two modules referencing the bundle via path while others use a git URL is - what produced uv's "conflicting URLs" abort on a single co-install command. """ data = _load_pyproject() sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {}) @@ -94,3 +91,24 @@ def test_allow_direct_references_enabled(self) -> None: "tool.hatch.metadata.allow-direct-references must be true to build a wheel " f"carrying the direct git reference, got: {allow!r}" ) + + def test_entry_point_module_id_is_correct(self) -> None: + """Entry point key must match the module ID used in graph-analyst.md.""" + data = _load_pyproject() + eps = data.get("project", {}).get("entry-points", {}).get("amplifier.modules", {}) + assert "tool-context-intelligence-query" in eps, ( + f"Entry point 'tool-context-intelligence-query' not found in " + f"[project.entry-points.'amplifier.modules']: {eps}" + ) + assert ( + "amplifier_module_tool_context_intelligence_query" + in eps["tool-context-intelligence-query"] + ) + + def test_asyncio_mode_is_auto(self) -> None: + """pytest-asyncio must be in auto mode for the test suite to run correctly.""" + data = _load_pyproject() + asyncio_mode = ( + data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("asyncio_mode") + ) + assert asyncio_mode == "auto", f"asyncio_mode must be 'auto', got: {asyncio_mode!r}" diff --git a/modules/tool-context-intelligence-query/uv.lock b/modules/tool-context-intelligence-query/uv.lock new file mode 100644 index 0000000..3dd2c90 --- /dev/null +++ b/modules/tool-context-intelligence-query/uv.lock @@ -0,0 +1,520 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "amplifier-bundle-context-intelligence" +version = "0.1.1" +source = { git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=main#2e47dc7c331b60210814f711129a10fdffd76ee4" } + +[[package]] +name = "amplifier-core" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tomli" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/90/d520390cd91aae3d02db53653f828046089c79203dbb142e9bda346fa1d6/amplifier_core-1.6.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d35130e4262cf0db2d6c5f7e65e244a9ef2c7397bfe2a9853bc9b0d9fd05be64", size = 8113151, upload-time = "2026-05-18T16:13:46.825Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3ab3126ba5a6f2fc6051a4d08e42364899e4c9ac4daa9d0a60947bf8acd1/amplifier_core-1.6.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:387a2c58fcf4caefdb45c52ec228307bc225e73606897f242154782bc3e123da", size = 7268223, upload-time = "2026-05-18T16:13:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/21/22/5a36160b3487170bcba0cbc61535101ff624e8314ed38fd35e561cb711a1/amplifier_core-1.6.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8344fccdedd725a51c018de17867cdf1c35abb571dabc0bbccdb5c1242324a47", size = 7532259, upload-time = "2026-05-18T16:13:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/3874c2308523209411367cf3b8b690e14e869f5f6bfb64cb1b1971e06a96/amplifier_core-1.6.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a8e0103242a2e2a975c880b1de0e5a02501e0421c1e5386dadae3f111e1d2b5", size = 8507642, upload-time = "2026-05-18T16:13:52.977Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/3646a89537b4556274183519f6db9c354fb3d183f52ef4a2179af12dd386/amplifier_core-1.6.0-cp311-abi3-win_amd64.whl", hash = "sha256:5113aa2d88038776eb257af9e7d9de7af13b3cd9097d2ac67aef5730fa0678e3", size = 8910313, upload-time = "2026-05-18T16:13:55.249Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/58b141115e5eea65703f0b01459eefed36b561e9642ba96d48542345cd8f/amplifier_core-1.6.0-cp311-abi3-win_arm64.whl", hash = "sha256:e1b2731dc09d1cbc668b411007e7f9a2c7edbd75b2525407cae1e6b4a4de0b83", size = 7661416, upload-time = "2026-05-18T16:13:57.513Z" }, +] + +[[package]] +name = "amplifier-module-tool-context-intelligence-query" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "amplifier-bundle-context-intelligence" }, + { name = "httpx" }, + { name = "idna" }, +] + +[package.dev-dependencies] +dev = [ + { name = "amplifier-core" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "amplifier-bundle-context-intelligence", git = "https://github.com/microsoft/amplifier-bundle-context-intelligence?rev=main" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "idna", specifier = ">=3.15" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "amplifier-core", specifier = ">=1.6.0" }, + { name = "pyright", specifier = ">=1.1" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, + { name = "ruff", specifier = ">=0.4" }, +] + +[[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 = "anyio" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[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 = "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 = "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 = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "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 = "pydantic" +version = "2.13.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[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 = "pyright" +version = "1.1.410" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, + { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, + { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, + { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[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" }, +] diff --git a/modules/tool-graph-query/amplifier_module_tool_graph_query/__init__.py b/modules/tool-graph-query/amplifier_module_tool_graph_query/__init__.py deleted file mode 100644 index baa50c3..0000000 --- a/modules/tool-graph-query/amplifier_module_tool_graph_query/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Graph query tool module — Cypher queries against the context-intelligence server. - -Implements the Amplifier Tool protocol. Configuration is resolved lazily -via the ``context_intelligence.config_resolver`` coordinator capability -registered by the hook-context-intelligence module. -""" - -from __future__ import annotations - -from typing import Any - -__amplifier_module_type__ = "tool" - - -async def mount(coordinator: Any, config: dict[str, Any]) -> dict[str, Any]: # noqa: ARG001 - """Mount the graph_query tool. - - Captures a coordinator reference for lazy capability resolution. - The tool reads the config resolver at execute() time, not mount() time, - because hooks mount after tools. - """ - from .graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=coordinator) - await coordinator.mount("tools", tool, name=tool.name) - return {"tool": tool.name, "status": "mounted"} diff --git a/modules/tool-graph-query/tests/__init__.py b/modules/tool-graph-query/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/tool-graph-query/tests/test_graph_query_tool.py b/modules/tool-graph-query/tests/test_graph_query_tool.py deleted file mode 100644 index ad62614..0000000 --- a/modules/tool-graph-query/tests/test_graph_query_tool.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Tests for GraphQueryTool — migrated to AsyncCIClient (Task 10).""" - -from __future__ import annotations - -from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch - - -# --------------------------------------------------------------------------- -# Shared helpers -# --------------------------------------------------------------------------- - - -def _make_coordinator(resolver: Any = None) -> MagicMock: - coordinator = MagicMock() - coordinator.get_capability = MagicMock(return_value=resolver) - return coordinator - - -def _make_resolver( - server_url: str | None = "http://localhost:8080", - workspace: str = "test-workspace", - api_key: str | None = "test-api-key", -) -> MagicMock: - resolver = MagicMock() - resolver.context_intelligence_server_url = server_url - resolver.workspace = workspace - resolver.context_intelligence_api_key = api_key - return resolver - - -def _make_mock_async_ci_client(return_value: Any = None): - """Return (mock_instance, mock_cls) for patching AsyncCIClient.""" - mock_instance = AsyncMock() - mock_instance.cypher = AsyncMock(return_value=return_value if return_value is not None else []) - mock_cls = MagicMock(return_value=mock_instance) - return mock_instance, mock_cls - - -# --------------------------------------------------------------------------- -# TestGraphQueryToolProtocol -# --------------------------------------------------------------------------- - - -class TestGraphQueryToolProtocol: - """Tool protocol surface tests.""" - - def test_name_is_graph_query(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - assert tool.name == "graph_query" - - def test_description_mentions_cypher(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - assert "Cypher" in tool.description - - def test_description_mentions_context_intelligence(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - assert "context-intelligence" in tool.description - - def test_input_schema_returns_object_type(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - assert tool.input_schema["type"] == "object" - - def test_input_schema_has_query_as_required(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - assert "query" in tool.input_schema["required"] - - def test_input_schema_has_optional_params_and_workspace(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - tool = GraphQueryTool(coordinator=_make_coordinator()) - props = tool.input_schema["properties"] - assert "params" in props - assert "workspace" in props - # Neither should be required - assert "params" not in tool.input_schema["required"] - assert "workspace" not in tool.input_schema["required"] - - async def test_execute_returns_tool_result(self) -> None: - from amplifier_core.models import ToolResult - - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n) RETURN n LIMIT 1"}) - - assert isinstance(result, ToolResult) - - -# --------------------------------------------------------------------------- -# TestLazyCapabilityResolution -# --------------------------------------------------------------------------- - - -class TestLazyCapabilityResolution: - """Lazy resolver lookup and caching behaviour.""" - - async def test_capability_not_found_returns_configuration_error(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - coordinator = _make_coordinator(resolver=None) - tool = GraphQueryTool(coordinator=coordinator) - result = await tool.execute({"query": "MATCH (n) RETURN n"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "configuration_error" - - async def test_server_url_none_returns_configuration_error(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(server_url=None) - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - result = await tool.execute({"query": "MATCH (n) RETURN n"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "configuration_error" - - async def test_resolver_cached_after_first_lookup(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n LIMIT 1"}) - await tool.execute({"query": "MATCH (n) RETURN n LIMIT 2"}) - - # get_capability should only be called once (on first execute) - coordinator.get_capability.assert_called_once_with("context_intelligence.config_resolver") - - async def test_configured_resolver_succeeds(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n) RETURN n"}) - - assert result.success is True - - -# --------------------------------------------------------------------------- -# TestGraphQuery -# --------------------------------------------------------------------------- - - -class TestGraphQuery: - """AsyncCIClient construction and delegation tests.""" - - async def test_client_constructed_with_server_url_and_api_key(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(server_url="http://ci-server:9000", api_key="my-key") - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n"}) - - mock_cls.assert_called_once() - call_kwargs = mock_cls.call_args.kwargs - assert call_kwargs.get("server_url") == "http://ci-server:9000" - assert call_kwargs.get("api_key") == "my-key" - - async def test_workspace_injected_into_cypher_call(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(workspace="my-workspace") - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n"}) - - cypher_args = mock_instance.cypher.call_args - assert cypher_args is not None - # workspace is the 2nd positional arg: cypher(query, workspace) - all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) - assert "my-workspace" in all_args - - async def test_result_forwarded_from_cypher_call(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - expected = [{"n": {"id": "session-1"}}] - mock_instance, mock_cls = _make_mock_async_ci_client(return_value=expected) - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n:Session) RETURN n LIMIT 10"}) - - assert result.success is True - assert result.output == expected - - -# --------------------------------------------------------------------------- -# TestGraphQueryWorkspaceOverride -# --------------------------------------------------------------------------- - - -class TestGraphQueryWorkspaceOverride: - """Per-call workspace override behaviour.""" - - async def test_per_call_workspace_override(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(workspace="default-workspace") - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n", "workspace": "override-workspace"}) - - cypher_args = mock_instance.cypher.call_args - all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) - assert "override-workspace" in all_args - - async def test_wildcard_workspace_override(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(workspace="default-workspace") - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n", "workspace": "*"}) - - cypher_args = mock_instance.cypher.call_args - all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) - assert "*" in all_args - - async def test_default_workspace_from_resolver(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(workspace="resolver-workspace") - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - # No workspace key in input — should fall back to resolver's workspace - await tool.execute({"query": "MATCH (n) RETURN n"}) - - cypher_args = mock_instance.cypher.call_args - all_args = list(cypher_args.args) + list(cypher_args.kwargs.values()) - assert "resolver-workspace" in all_args - - -# --------------------------------------------------------------------------- -# TestGraphQueryErrors -# --------------------------------------------------------------------------- - - -class TestGraphQueryErrors: - """Error path tests — AsyncCIClient.cypher() returns [] on HTTP failure (graceful degradation).""" - - async def test_server_error_returns_success_with_empty_result(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - # AsyncCIClient.cypher() returns [] on HTTP error (graceful degradation) - mock_instance, mock_cls = _make_mock_async_ci_client(return_value=[]) - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n) RETURN n"}) - - assert result.success is True - assert result.output == [] - - async def test_none_api_key_passed_as_empty_string(self) -> None: - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver(api_key=None) - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n) RETURN n"}) - - # Should succeed and pass empty string as api_key - assert result.success is True - call_kwargs = mock_cls.call_args.kwargs - assert call_kwargs.get("api_key") == "" - - -# --------------------------------------------------------------------------- -# TestGraphQueryParamsForwarding — regression for params wiring bug -# --------------------------------------------------------------------------- - - -class TestGraphQueryParamsForwarding: - """Regression: user-supplied params reach AsyncCIClient.cypher().""" - - async def test_params_are_forwarded_to_async_client_cypher(self) -> None: - """params={...} from tool input must be forwarded as a kwarg to cypher().""" - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute( - { - "query": "MATCH (s:Session {id: $session_id}) RETURN s", - "params": {"session_id": "abc"}, - } - ) - - assert result.success is True - cypher_call = mock_instance.cypher.call_args - assert cypher_call is not None - # params must arrive as the 'params' keyword argument - assert cypher_call.kwargs.get("params") == {"session_id": "abc"} - - async def test_none_params_sends_empty_dict_to_cypher(self) -> None: - """Omitting params from tool input must default to {} at cypher().""" - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - mock_instance, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - await tool.execute({"query": "MATCH (n) RETURN n"}) - - cypher_call = mock_instance.cypher.call_args - assert cypher_call is not None - assert cypher_call.kwargs.get("params") == {} - - async def test_non_dict_params_returns_validation_error(self) -> None: - """Passing params as a non-dict must return a validation_error ToolResult.""" - from amplifier_module_tool_graph_query.graph_query_tool import GraphQueryTool - - resolver = _make_resolver() - coordinator = _make_coordinator(resolver=resolver) - tool = GraphQueryTool(coordinator=coordinator) - - _, mock_cls = _make_mock_async_ci_client() - with patch("amplifier_module_tool_graph_query.graph_query_tool.AsyncCIClient", mock_cls): - result = await tool.execute({"query": "MATCH (n) RETURN n", "params": "not-a-dict"}) - - assert result.success is False - assert result.error is not None - assert result.error["type"] == "validation_error" diff --git a/modules/tool-graph-query/tests/test_mount.py b/modules/tool-graph-query/tests/test_mount.py deleted file mode 100644 index d97b843..0000000 --- a/modules/tool-graph-query/tests/test_mount.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Tests for tool-graph-query module mount contract.""" - -from __future__ import annotations - -import inspect -from unittest.mock import AsyncMock, MagicMock - - -class TestModuleContract: - """Module-level contract for a tool module.""" - - def test_module_type_is_tool(self) -> None: - from amplifier_module_tool_graph_query import __amplifier_module_type__ - - assert __amplifier_module_type__ == "tool" - - def test_mount_is_coroutine(self) -> None: - from amplifier_module_tool_graph_query import mount - - assert inspect.iscoroutinefunction(mount) - - def test_mount_signature_has_coordinator_and_config(self) -> None: - from amplifier_module_tool_graph_query import mount - - sig = inspect.signature(mount) - params = list(sig.parameters.keys()) - assert params[0] == "coordinator" - assert params[1] == "config" - - -class TestMountBehavior: - """mount() registers a Tool-protocol-compliant object via coordinator.mount().""" - - async def test_mount_calls_coordinator_mount_with_tools_category(self) -> None: - from amplifier_module_tool_graph_query import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - coordinator.mount.assert_called_once() - assert coordinator.mount.call_args.args[0] == "tools" - - async def test_mounted_tool_has_name_graph_query(self) -> None: - from amplifier_module_tool_graph_query import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - assert coordinator.mount.call_args.kwargs["name"] == "graph_query" - - async def test_mounted_tool_is_protocol_compliant(self) -> None: - from amplifier_module_tool_graph_query import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - await mount(coordinator, config={}) - tool = coordinator.mount.call_args.args[1] - assert hasattr(tool, "name") - assert hasattr(tool, "description") - assert hasattr(tool, "input_schema") - assert hasattr(tool, "execute") - assert isinstance(tool.input_schema, dict) - assert inspect.iscoroutinefunction(tool.execute) - - async def test_mount_returns_metadata_dict(self) -> None: - from amplifier_module_tool_graph_query import mount - - coordinator = MagicMock() - coordinator.mount = AsyncMock() - result = await mount(coordinator, config={}) - assert isinstance(result, dict) - assert result["tool"] == "graph_query" - assert result["status"] == "mounted" diff --git a/modules/tool-graph-query/uv.lock b/modules/tool-graph-query/uv.lock deleted file mode 100644 index 8d2a6e2..0000000 --- a/modules/tool-graph-query/uv.lock +++ /dev/null @@ -1,520 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "amplifier-bundle-context-intelligence" -version = "0.1.1" -source = { editable = "../../" } - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "httpx", specifier = ">=0.25" }, - { name = "idna", specifier = ">=3.15" }, - { name = "pyright", specifier = ">=1.1" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "ruff", specifier = ">=0.4" }, -] - -[[package]] -name = "amplifier-core" -version = "1.2.4" -source = { git = "https://github.com/microsoft/amplifier-core?branch=main#7a99cbbcb6b191872e9ffd5cf4beed18a59511c2" } -dependencies = [ - { name = "click" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tomli" }, - { name = "typing-extensions" }, -] - -[[package]] -name = "amplifier-module-tool-graph-query" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "amplifier-bundle-context-intelligence" }, - { name = "httpx" }, - { name = "idna" }, -] - -[package.dev-dependencies] -dev = [ - { name = "amplifier-core" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "amplifier-bundle-context-intelligence", editable = "../../" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "idna", specifier = ">=3.15" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "amplifier-core", git = "https://github.com/microsoft/amplifier-core?branch=main" }, - { name = "pyright", specifier = ">=1.1" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "ruff", specifier = ">=0.4" }, -] - -[[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 = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "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 = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[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 = "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 = "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 = "idna" -version = "3.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[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 = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[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 = "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 = "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/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[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 = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - -[[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" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -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 = "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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - -[[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" }, -] diff --git a/tests/test_tool_resolver.py b/tests/test_tool_resolver.py new file mode 100644 index 0000000..ca8e4fb --- /dev/null +++ b/tests/test_tool_resolver.py @@ -0,0 +1,272 @@ +"""Unit tests for context_intelligence.tool_resolver. + +Tests the shared helpers (_first_entry, _first_destination, resolve_query_endpoint) +and ToolConfigResolver.sources (including legacy synthesis — spec §7 cases +#9 and #10 at the unit level). +""" + +from __future__ import annotations + +import os +from types import SimpleNamespace +from typing import Any +from unittest.mock import MagicMock, patch + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_coordinator(config: dict | None = None) -> MagicMock: + """MagicMock coordinator with a real .config dict (not a Mock auto-attr).""" + coordinator = MagicMock() + coordinator.config = config if config is not None else {} + return coordinator + + +def _make_dest(url: str, api_key: str, name: str = "default") -> SimpleNamespace: + """Destination-like SimpleNamespace.""" + return SimpleNamespace(name=name, url=url, api_key=api_key) + + +# --------------------------------------------------------------------------- +# TestFirstEntry +# --------------------------------------------------------------------------- + + +class TestFirstEntry: + """_first_entry() — edge-case coverage.""" + + def test_none_returns_none(self) -> None: + from context_intelligence.tool_resolver import _first_entry + + assert _first_entry(None) is None + + def test_empty_dict_returns_none(self) -> None: + from context_intelligence.tool_resolver import _first_entry + + assert _first_entry({}) is None + + def test_non_dict_mock_returns_none(self) -> None: + """A MagicMock (auto-created attribute) is not a dict → None.""" + from context_intelligence.tool_resolver import _first_entry + + assert _first_entry(MagicMock()) is None + + def test_single_entry_returns_value(self) -> None: + from context_intelligence.tool_resolver import _first_entry + + result = _first_entry({"alpha": "hello"}) + assert result == "hello" + + def test_two_entry_dict_returns_first(self) -> None: + from context_intelligence.tool_resolver import _first_entry + + result = _first_entry({"alpha": 1, "beta": 2}) + assert result == 1 + + +# --------------------------------------------------------------------------- +# TestFirstDestination +# --------------------------------------------------------------------------- + + +class TestFirstDestination: + """_first_destination() — delegating edge cases.""" + + def test_none_resolver_returns_none(self) -> None: + from context_intelligence.tool_resolver import _first_destination + + assert _first_destination(None) is None + + def test_resolver_with_empty_destinations_returns_none(self) -> None: + from context_intelligence.tool_resolver import _first_destination + + resolver = MagicMock() + resolver.destinations = {} + assert _first_destination(resolver) is None + + def test_resolver_with_no_destinations_attr_returns_none(self) -> None: + """getattr(resolver, 'destinations', None) returns None for a plain object.""" + from context_intelligence.tool_resolver import _first_destination + + class NoDestinations: + pass + + assert _first_destination(NoDestinations()) is None + + +# --------------------------------------------------------------------------- +# TestResolveQueryEndpointHelpers +# --------------------------------------------------------------------------- + + +class TestResolveQueryEndpoint: + """resolve_query_endpoint() — the three-tier chain.""" + + def _make_empty_tool_resolver(self) -> Any: + from context_intelligence.tool_resolver import ToolConfigResolver + + return ToolConfigResolver({}, _make_coordinator()) + + async def test_none_hook_and_empty_tool_resolver_returns_none_tuple(self) -> None: + from context_intelligence.tool_resolver import resolve_query_endpoint + + tool_resolver = self._make_empty_tool_resolver() + # Clear all CI env vars so tier-3 fallback cannot supply a URL + clean = {k: "" for k in os.environ if k.startswith("AMPLIFIER_CONTEXT_INTELLIGENCE_")} + with patch.dict(os.environ, clean): + url, api_key = resolve_query_endpoint(None, tool_resolver) + assert url is None + assert api_key is None + + async def test_debug_log_emitted(self, caplog: Any) -> None: + import logging + + from context_intelligence.tool_resolver import resolve_query_endpoint + + tool_resolver = self._make_empty_tool_resolver() + with caplog.at_level(logging.DEBUG, logger="context_intelligence.tool_resolver"): + resolve_query_endpoint(None, tool_resolver) + + assert any("CI query endpoint resolved" in r.message for r in caplog.records) + + +# --------------------------------------------------------------------------- +# TestToolConfigResolverSources +# --------------------------------------------------------------------------- + + +class TestToolConfigResolverSources: + """ToolConfigResolver.sources — parsing and legacy synthesis.""" + + def test_explicit_mapping_parsed(self) -> None: + """Explicit sources key → parsed into Source dict.""" + from context_intelligence.tool_resolver import Source, ToolConfigResolver + + config = { + "sources": { + "default": {"url": "http://read.example.com", "api_key": "read-key"}, + } + } + resolver = ToolConfigResolver(config, _make_coordinator()) + rd = resolver.sources + + assert "default" in rd + assert isinstance(rd["default"], Source) + assert rd["default"].url == "http://read.example.com" + assert rd["default"].api_key == "read-key" + assert rd["default"].name == "default" + + def test_explicit_empty_dict_returns_empty(self) -> None: + """Explicit sources={} → {} (no legacy synthesis).""" + from context_intelligence.tool_resolver import ToolConfigResolver + + config = {"sources": {}} + resolver = ToolConfigResolver(config, _make_coordinator()) + assert resolver.sources == {} + + def test_non_dict_entry_skipped(self) -> None: + """Non-dict entries under sources are silently skipped.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + config = { + "sources": { + "bad": "not-a-dict", + "good": {"url": "http://read.example.com", "api_key": "k"}, + } + } + resolver = ToolConfigResolver(config, _make_coordinator()) + rd = resolver.sources + assert "bad" not in rd + assert "good" in rd + + def test_url_api_key_stripped(self) -> None: + """Whitespace in url/api_key is stripped.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + config = { + "sources": { + "default": {"url": " http://read.example.com ", "api_key": " key "}, + } + } + resolver = ToolConfigResolver(config, _make_coordinator()) + rd = resolver.sources + assert rd["default"].url == "http://read.example.com" + assert rd["default"].api_key == "key" + + # --- Case #9: legacy synthesis when BOTH scalars present --- + + def test_absent_key_both_scalars_synthesizes_default(self) -> None: + """Case #9 unit: absent sources key + both scalars → synthesized default.""" + from context_intelligence.tool_resolver import Source, ToolConfigResolver + + config = { + "context_intelligence_server_url": "http://legacy.example.com", + "context_intelligence_api_key": "legacy-key", + } + resolver = ToolConfigResolver(config, _make_coordinator()) + rd = resolver.sources + + assert "default" in rd + assert isinstance(rd["default"], Source) + assert rd["default"].url == "http://legacy.example.com" + assert rd["default"].api_key == "legacy-key" + + # --- Case #10: url-only → no synthesis --- + + def test_absent_key_url_only_no_synthesis(self) -> None: + """Case #10 unit: absent sources key + url only → {} (no synthesis).""" + from context_intelligence.tool_resolver import ToolConfigResolver + + config = { + "context_intelligence_server_url": "http://legacy.example.com", + # no context_intelligence_api_key + } + resolver = ToolConfigResolver(config, _make_coordinator()) + assert resolver.sources == {} + + def test_absent_key_no_scalars_returns_empty(self) -> None: + """Absent key + no scalars → {} (neither synthesis nor parse).""" + from context_intelligence.tool_resolver import ToolConfigResolver + + resolver = ToolConfigResolver({}, _make_coordinator()) + assert resolver.sources == {} + + def test_sources_cached_after_first_access(self) -> None: + """sources is cached — same object returned on second access.""" + from context_intelligence.tool_resolver import ToolConfigResolver + + config = { + "sources": { + "default": {"url": "http://read.example.com", "api_key": "k"}, + } + } + resolver = ToolConfigResolver(config, _make_coordinator()) + rd1 = resolver.sources + rd2 = resolver.sources + assert rd1 is rd2 + + # --- env is excluded from legacy synthesis (tier 3 only) --- + + def test_absent_key_env_does_not_synthesize_into_tier1(self) -> None: + """Env vars are excluded from legacy synthesis — env can never enter tier 1. + + Setting canonical env vars alone does NOT cause legacy synthesis to fire. + Env is only consulted at tier 3 in resolve_query_endpoint(), which is + BELOW the hook destination (tier 2). This prevents env from outranking + an upload destination via the synthesis path. + """ + from context_intelligence.tool_resolver import ToolConfigResolver + + env_patch = { + "AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL": "http://env-scalar.example.com", + "AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY": "env-scalar-key", + } + with patch.dict(os.environ, env_patch): + resolver = ToolConfigResolver({}, _make_coordinator()) + rd = resolver.sources + + # Env must NOT synthesize into sources — result is empty dict. + assert rd == {}