diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/RIGHTS.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/RIGHTS.md
new file mode 100644
index 0000000..ee66531
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/RIGHTS.md
@@ -0,0 +1,39 @@
+# Submission rights declaration
+
+Project: Life Exchange Rate MCP
+
+Submission slug: `kongkoukk-life-exchange-rate`
+
+Submitter: **kongkoukk**
+
+Date: **2026-09-16**
+
+GitHub account: [KongkouKK](https://github.com/KongkouKK)
+
+Public support contact: **haozlee1994@gmail.com**
+
+Source repository: https://github.com/KongkouKK/life-exchange-rate-mcp
+
+Review commit: `238954196ea6922d3cea2ae8acb914b0f4e01dbb`
+
+The submitter confirms that they own, or have sufficient authorization for, the source code, dependencies, service, data, branding, and other materials submitted in this pull request.
+
+Subject to the official program terms, the submitter authorizes X-Agent to retain, reproduce, audit, test, archive, and publish the submitted program artifact for judging, fraud prevention, dispute handling, ecosystem submission, and post-award accountability. Closing the pull request, deleting a fork, or deleting an external repository does not revoke the official archive rights attached to an accepted and rewarded entry.
+
+## First-party materials
+
+No general-purpose open-source license has been selected for the first-party project. The submitter grants the explicit submission, review, archive and related authorization stated above under the official program terms. This declaration does not assign MIT, Apache or another general-purpose license to first-party code.
+
+## Third-party components and data
+
+The [dependency inventory](verification/licenses/DEPENDENCIES.md) records the lockfile versions, inspected license metadata and evidence scope. [Original dependency notices](verification/licenses/THIRD_PARTY_NOTICES.txt) preserve the available upstream license, copyright and author texts. [Provider terms and supplementary upstream declarations](verification/licenses/PROVIDER-TERMS.md) record applicable source links, attribution requirements, exceptions and the limits of the inspection.
+
+The source uses ordinary dependencies through its manifests and lockfile. Generated dependency installations and build outputs are excluded from the review source. No blanket ownership over dependency implementations, upstream data, RSS text, names or marks is claimed.
+
+The implementation identifies Frankfurter's ECB reference-rate route, ECB structured policy-rate observations, Federal Reserve/ECB RSS, and optional EIA Brent observations. Their roles and data boundaries are documented in [data sources](source/docs/DATA_SOURCES.md). Preserve source attribution, observation dates and the distinction between provider observations, synthetic fixtures and project calculations.
+
+## Exceptions and restrictions
+
+Applicable third-party licenses, copyright notices, attribution requirements, provider terms and restrictions on third-party material or marks remain in force. The submission authorization does not replace those terms or grant rights the submitter cannot grant. The license and provider records identify their evidence limits, including the unverified exact isolated build-backend version; they are not a claim of unlimited rights over all upstream material.
+
+This declaration follows the [official program rights template](https://github.com/xagentAI/xagt-plugin/blob/main/submissions/RIGHTS_TEMPLATE.md) and was authorized by the submitter for this submission.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/SUBMISSION.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/SUBMISSION.md
new file mode 100644
index 0000000..cc386cb
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/SUBMISSION.md
@@ -0,0 +1,78 @@
+# Life Exchange Rate MCP
+
+## Capability
+
+- **One-line description:** Translate structured macroeconomic changes into travel purchasing power, interest/fuel scenarios, work hours and user-defined everyday units.
+- **Who it helps:** Agents explaining a market move for a user who supplies income, prices, budgets and relevant exposures.
+- **Capability boundary:** Headlines trigger attention; numerical observations or labeled scenarios drive deterministic arithmetic. The service does not execute trades/payments, forecast prices, infer household income or claim that a separately retrieved headline caused a market move.
+
+## Live API
+
+- **API base URL:** https://life-exchange-rate-mcp.vercel.app
+- **Health check:** https://life-exchange-rate-mcp.vercel.app/health
+- **Deployment proof:** https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json
+- **MCP endpoint:** https://life-exchange-rate-mcp.vercel.app/mcp/ — including the trailing slash.
+- **Authentication:** None for the public demonstration. No reviewer credential is required.
+- **Rate limits / known limits:** No application-level per-user quota is implemented. Free Vercel Hobby quotas, cold starts and upstream availability can affect service. The configured function duration is at most 30 seconds. MCP uses stateless HTTP/JSON; no availability guarantee or paid capacity is claimed.
+- **API contract:** [Live OpenAPI](https://life-exchange-rate-mcp.vercel.app/openapi.json), [checked-in OpenAPI](source/verification/openapi.json), [tool contracts](source/docs/TOOL_CONTRACTS.md).
+
+## Source and reproducibility
+
+- **Source repository:** https://github.com/KongkouKK/life-exchange-rate-mcp
+- **Review commit:** `238954196ea6922d3cea2ae8acb914b0f4e01dbb`
+- **Source submitted in this PR:** `source/`, including entry point, package files, fixtures, tests, `pyproject.toml`, `uv.lock` and configuration example.
+- **Source comparison:** The public archive's 76 files matched the prepared source with text line-ending differences ignored; no missing, differing or extra paths were found. See [source verification](verification/source-verification-final.json).
+
+From this submission directory, enter the runnable project and run:
+
+```bash
+cd source
+uv sync --locked --extra dev
+uv run --frozen python -m pytest
+uv run --frozen uvicorn life_exchange_rate.main:app --app-dir source --host 127.0.0.1 --port 8000
+```
+
+From that runnable project directory, the standalone protocol checks are:
+
+```bash
+uv run --frozen python verification/smoke_mcp.py
+uv run --frozen python verification/smoke_mcp.py --live-fx
+```
+
+Each smoke invocation starts and stops its own local server. The first uses fixtures; the second also requires live provider access.
+
+**Deploy:** Import the public source repository into Vercel Hobby, keeping the repository root, FastAPI framework, root `app.py` and the checked-in dependency lock. Keep system environment variables exposed. Use the public production domain; no protected-preview access is needed. See [deployment instructions](source/docs/DEPLOY_FREE.md). The build requirement names Hatchling without a pinned build-backend version; its exact isolated Vercel version has not been verified.
+
+**Version binding:** The app reads `VERCEL_GIT_COMMIT_SHA` and exposes it in both `/health` and the same-origin deployment proof. Both production endpoints were verified against the exact review commit above. A later source revision requires fresh version binding and verification before changing the review baseline.
+
+## Verification
+
+The project passed **148 local tests**. The hosted run at **2026-09-16 09:55:43 UTC** verified the declared review commit and passed:
+
+- Current protocol `2026-07-28` and legacy protocol `2025-11-25`: each listed **9 tools**, passed **12 positive calls** covering all nine tools and rejected **7 invalid-input cases**.
+- Live FX and official Fed RSS calls. Policy-rate and energy calls used clearly labeled synthetic fixtures; no live EIA test is claimed.
+- Synthetic FX calculation: **1,739.13 SEK / 8.70 work hours** at the declared synthetic household inputs.
+- Captured live FX calculation: source dates 2026-09-08 to 2026-09-15, rates 16.0689 to 15.8648 JPY/SEK; a synthetic 20,000 SEK budget produced **257.30 SEK / 1.29 work hours**. Later live observations can differ.
+
+The public REST run at **2026-09-16 10:00:54 UTC** returned HTTP 200 with the expected fixture values/provenance and HTTP 422 for invalid input, with the same commit before and after the calls.
+
+See the [repeatable verification runbook](verification/README.md), [complete hosted MCP record](verification/hosted-live-verification-final.json), [deterministic hosted record](verification/hosted-verification-final.json), and [REST record](verification/hosted-rest-verification-final.json).
+
+**Safe error behavior:** Invalid REST input returns HTTP 422; unavailable live providers return sanitized HTTP 502 responses. Invalid MCP calls return tool-error results. Policy/energy `live_or_fixture` mode can return an explicitly labeled synthetic fallback; `live` mode does not present such a fallback as a successful observation.
+
+These are application and deployment results. Official package validation is a separate pre-publication check; no unexecuted official check is represented as passed.
+
+## Security and data handling
+
+- **Data collected as request inputs:** Home currency/country, income, working hours, travel budget/currency, optional mortgage/savings/fuel exposures, everyday-unit prices, numeric events and pass-through assumptions.
+- **Purpose and retention:** Calculate the requested result. The application has no database or application-level persistence of household profiles. Vercel processes API traffic; hosting-layer request/log retention follows its settings, and no verified retention interval is asserted. Review profiles are synthetic.
+- **Outbound services:** Frankfurter's ECB FX route, ECB data API, Fed/ECB RSS, and EIA API when a key is configured. Providers receive currency/date/series queries, not the household income, mortgage or everyday-price profile.
+- **Secrets:** Optional `EIA_API_KEY` belongs only in the deployment environment. No credentials are included in this submission. No EIA key is needed for the synthetic judging flow.
+- **Known restrictions:** ECB daily reference rates are not executable retail quotes; retail fees/spreads are excluded. Interest/fuel pass-through is a stated scenario, with currency compatibility enforced. Free hosting and providers can be unavailable. [Provider terms](verification/licenses/PROVIDER-TERMS.md), attribution and third-party exceptions remain applicable.
+
+## Support
+
+- **Submitter:** kongkoukk
+- **GitHub:** [KongkouKK](https://github.com/KongkouKK)
+- **Public contact:** haozlee1994@gmail.com
+- **License / rights:** [RIGHTS.md](RIGHTS.md) records the submitter's authorized program review/archive/publishing declaration and preserves third-party terms. No general-purpose first-party open-source license has been selected.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.env.example b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.env.example
new file mode 100644
index 0000000..c82c373
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.env.example
@@ -0,0 +1,8 @@
+# Local/self-hosted override. Vercel Git deployments use the actual platform SHA.
+REVIEW_COMMIT=dev-local
+SUBMISSION_SLUG=kongkoukk-life-exchange-rate
+# Leave unset on Vercel to use VERCEL_PROJECT_PRODUCTION_URL.
+# PUBLIC_BASE_URL=https://YOUR-VERIFIED-PRODUCTION-HOST
+FRANKFURTER_BASE_URL=https://api.frankfurter.dev
+# Optional; no key is needed for FX, ECB or fixture judging.
+EIA_API_KEY=
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.gitignore b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.gitignore
new file mode 100644
index 0000000..09be133
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.gitignore
@@ -0,0 +1,12 @@
+.venv/
+__pycache__/
+.pytest_cache/
+*.pyc
+.env
+.DS_Store
+
+.vercel/
+.env.*
+!.env.example
+dist/
+build/
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.python-version b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.python-version
new file mode 100644
index 0000000..3d2704b
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.vercelignore b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.vercelignore
new file mode 100644
index 0000000..9d68ce8
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/.vercelignore
@@ -0,0 +1,10 @@
+.git
+.venv
+__pycache__
+.pytest_cache
+*.pyc
+tests
+docs
+verification
+.env
+.env.*
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/CHANGELOG.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/CHANGELOG.md
new file mode 100644
index 0000000..2f88ba7
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Changelog
+
+## 0.2.0 continuation
+
+- Installed and locked the current MCP SDK v2 dependencies; verified the `/mcp/` mount and scoped deployment hostname protection.
+- Added real-client Streamable HTTP smoke tooling for current and legacy protocols, including invalid input rejection.
+- Added calendar-correct live FX windows, preceding equal-horizon volatility scores and transparent history/zero-volatility fallbacks.
+- Added structured ECB policy-rate and EIA Brent adapters with replaceable HTTP transports, explicit synthetic fixtures and sanitized fallback behavior.
+- Preserved event provenance in impact results; corrected inverse-FX trace units, percent factors and foreign-mortgage currency handling.
+- Enforced finite numeric inputs, consistent price changes, timestamp zones and scenario labels; exposed pass-through assumptions over MCP.
+- Fixed demo dates and labels; added a complete event/radar/travel/work/life-unit demo, checked OpenAPI and verification fixtures.
+
+Public deployment and review Git SHA remain pending real submission configuration.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/DEPLOYMENT_STATUS.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/DEPLOYMENT_STATUS.md
new file mode 100644
index 0000000..6108dfb
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/DEPLOYMENT_STATUS.md
@@ -0,0 +1,26 @@
+# Deployment status
+
+Updated 2026-09-16. The application is publicly deployed for GitHub account KongkouKK on Vercel Hobby, for a personal, noncommercial demonstration.
+
+- Source repository: [KongkouKK/life-exchange-rate-mcp](https://github.com/KongkouKK/life-exchange-rate-mcp).
+- Production origin: [life-exchange-rate-mcp.vercel.app](https://life-exchange-rate-mcp.vercel.app).
+- [API documentation](https://life-exchange-rate-mcp.vercel.app/docs), [health](https://life-exchange-rate-mcp.vercel.app/health), and [deployment proof](https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json) were checked successfully. Use the proof or health response to read the currently deployed Git revision; a documentation commit is not the deployment's source of truth.
+- Canonical MCP endpoint: `https://life-exchange-rate-mcp.vercel.app/mcp/`. Current and legacy protocol initialization passed, and both modes listed and called all nine tools.
+
+## Verification completed
+
+The application and three deployment configuration checks passed together: **148 local tests**. The earlier `verification/summary.json` and `build-check.json` describe the original local stage (145 tests and its wheel); they remain historical evidence, not proof of a Vercel build.
+
+The complete hosted run at **2026-09-16 06:40 UTC** passed protocol versions `2026-07-28` and `2025-11-25`. Each mode listed all nine tools, made 12 successful calls covering every tool, and rejected seven invalid inputs: unsupported event mode, unknown headline source, identical FX currencies, out-of-range lookback, a headline lacking numeric event values, conversion currency mismatch, and an invalid scenario multiplier.
+
+Live ECB/Frankfurter FX observations and Fed RSS were fetched successfully. Policy-rate and energy tools used synthetic fixtures; this is not evidence of hosted live policy-rate or EIA calls. The observed FX chain covered event retrieval → ranking → personal impact → life-unit conversion. Rates changed from 16.0689 to 15.8648 JPY/SEK between September 8 and 15; the explicit synthetic 20,000 SEK travel profile needed **257.30 SEK / 1.29 work hours** to restore its earlier JPY purchasing power. The separately fetched Fed headline was not asserted to cause the move.
+
+The [saved hosted run](verification/hosted-live-2026-09-16.json) includes health/proof responses before and after the calls, with the same expected source revision and production origin throughout. It is a historical record bound to the revision inside that JSON. Query the public deployment proof for the current revision, and capture fresh evidence when reviewing a later deployment.
+
+An earlier hosted fixture run at 06:30 UTC used MCP SDK 2.2.0 and returned the fixed result **1,739.13 SEK / 8.70 work hours**. Its health request returned HTTP 200 in 406 ms and deployment proof returned HTTP 200 in 297 ms, without redirects and within a 10-second request timeout. The source revision matched the expected deployed revision, and proof origin, slug, and MCP URL matched the production configuration. These are measurements of that run, not a guarantee of future latency or cold-start behavior.
+
+## Free plan and remaining submission work
+
+Vercel Hobby is the selected free plan, subject to its personal/noncommercial terms and quotas. No paid upgrade is required for this demonstration. No EIA key is configured, so energy results use an explicitly labeled synthetic scenario fallback; live EIA validation remains unperformed. See [free deployment instructions](docs/DEPLOY_FREE.md).
+
+Deployment does not complete contest submission. `submission.json` and `SUBMISSION.md` still need final review against the deployed proof, and `RIGHTS.md` requires the submitter's declarations. **No official contest pull request has been submitted.**
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/Dockerfile b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/Dockerfile
new file mode 100644
index 0000000..feef0a6
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.13-slim
+WORKDIR /app
+RUN pip install --no-cache-dir uv==0.12.13
+COPY pyproject.toml uv.lock ./
+COPY source ./source
+RUN uv sync --locked --no-dev --no-editable
+ENV PATH="/app/.venv/bin:$PATH"
+ENV PORT=8000
+CMD ["sh", "-c", "uvicorn life_exchange_rate.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/README.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/README.md
new file mode 100644
index 0000000..1078354
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/README.md
@@ -0,0 +1,69 @@
+# Life Exchange Rate MCP
+
+**Every headline has a price. See yours.**
+
+Translate structured macroeconomic events into travel purchasing power, mortgage/savings and fuel scenarios, work time, and user-defined everyday units. Headlines trigger attention; **only structured numeric observations drive calculations**. Wages, budgets, prices and pass-through assumptions come from explicit inputs or clearly labeled demonstration defaults.
+
+## Public demo
+
+The application is deployed on Vercel Hobby for a personal, noncommercial demonstration:
+
+- [Interactive API documentation](https://life-exchange-rate-mcp.vercel.app/docs)
+- [Health and deployed source revision](https://life-exchange-rate-mcp.vercel.app/health)
+- [Deployment verification proof](https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json)
+- MCP endpoint: **https://life-exchange-rate-mcp.vercel.app/mcp/**, including the trailing slash. Connect with an MCP client; this is not a web page.
+
+On 2026-09-16, hosted health, deployment proof, and complete MCP checks passed in current and legacy protocol modes. Each mode called all nine tools successfully (12 positive calls) and rejected seven invalid inputs. The run fetched live ECB/Frankfurter FX observations and Fed RSS; policy-rate and energy calls used explicitly synthetic fixtures. The [saved hosted evidence](verification/hosted-live-2026-09-16.json) records that deployment's revision, while the deployment proof reports the current revision. See [deployment status](DEPLOYMENT_STATUS.md) and the [verification runbook](verification/README.md).
+
+## Run locally
+
+Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/).
+
+```powershell
+uv sync --locked --extra dev
+uv run --frozen python -m pytest
+uv run --frozen uvicorn life_exchange_rate.main:app --app-dir source --host 127.0.0.1 --port 8000
+```
+
+Open [API docs](http://localhost:8000/docs), [health](http://localhost:8000/health), or [demo radar](http://localhost:8000/v1/radar/demo). The canonical MCP endpoint is **http://localhost:8000/mcp/**, including the trailing slash. It is a protocol endpoint, not a web page.
+
+For local configuration, copy `.env.example` to `.env` and add `--env-file .env` to the Uvicorn command. Environment files are not loaded automatically. Never commit API keys. Vercel Git deployments use the platform's actual `VERCEL_GIT_COMMIT_SHA` and production domain automatically. Self-hosted deployments can set `PUBLIC_BASE_URL` and `REVIEW_COMMIT` explicitly. The MCP host allowlist uses the resolved public origin; `/health` and `/.well-known/xagent-verification.json` report the same source revision. `dev-local` is a development placeholder. See [free deployment instructions](docs/DEPLOY_FREE.md).
+
+## End-to-end demo
+
+```powershell
+# Reproducible synthetic judging scenario, no network or key required:
+uv run --frozen python -m life_exchange_rate.demo
+# Actual ECB/Frankfurter observations -> ranking -> SEK/JPY travel -> work/life units:
+uv run --frozen python -m life_exchange_rate.demo --live
+# Real MCP HTTP client in current and legacy protocol modes:
+uv run --frozen python verification/smoke_mcp.py --live-fx
+```
+
+The smoke script starts and stops a local server automatically. Without `--live-fx`, it checks the offline chain and explicitly records the live FX/RSS calls as skipped. Evidence is saved in `verification/mcp-smoke.json` and `verification/end-to-end-demo.json`.
+
+In the fixed synthetic scenario, 20,000 SEK bought 300,000 JPY at 15 JPY/SEK and buys 276,000 JPY at 13.8. Restoring the original purchasing power needs **1,739.13 SEK**, equivalent to **8.70 work hours**, **38.65 coffees**, or **12.42 lunches** at the declared demonstration income and prices. These alternatives are equivalents of the same amount, not additional expenses. They are not current market data or the user's finances.
+
+The hosted live capture on **2026-09-16** retrieved ECB/Frankfurter rates of **16.0689 → 15.8648 JPY/SEK** for **September 8 → 15**. For a synthetic person with a 20,000 SEK travel budget, restoring the earlier JPY purchasing power required **257.30 SEK**, equivalent to **1.29 work hours**, **5.72 coffees**, or **1.84 lunches** using explicit inputs of 32,000 SEK monthly income, 160 work hours, 45 SEK per coffee, and 140 SEK per lunch. These profile values are demonstration inputs, not the user's finances or observed retail prices; the reference-rate comparison excludes exchange fees and spreads. A Fed headline was fetched separately to test the headline interface, with no claim that it caused the FX move. [Capture and calculation trace](verification/hosted-live-2026-09-16.json).
+
+## Data and guarantees
+
+- Live FX uses Frankfurter's dedicated ECB route, correct calendar lookback selection and a rolling log-return anomaly score. Every score reports its horizon, reference sample count, mean, sample volatility and fallback reason. See `docs/FX_ANOMALY.md`.
+- Policy rates use the official ECB daily euro-area deposit facility series. Events apply only to matching EUR exposures.
+- Energy uses EIA API v2 Brent daily spot prices. `EIA_API_KEY` is needed only for live energy. No configured key means a labeled synthetic fallback in `live_or_fixture` mode.
+- Both structured adapters accept `live`, `fixture`, and `live_or_fixture`. Live mode fails when data are unavailable; fixture/fallback events have `confidence=scenario`, `metadata.synthetic=true`, no official evidence URL, fixed source dates, and a stated fallback reason when applicable.
+- Fed/ECB RSS returns headline triggers with `requires_quantification=true`; a headline cannot satisfy the numeric event schema.
+- Inputs reject nonfinite values, inconsistent price changes and nonpositive FX/oil observations. Rate calculations require matching currency scope; foreign-currency mortgages are excluded from home-currency totals.
+- Impact output preserves event provenance and source dates alongside formulas. Retail FX fees/spreads are excluded. Fuel and interest pass-through are explicit scenarios, not forecasts.
+
+## Interfaces
+
+Nine MCP tools cover events, headlines, live FX, policy rates, energy, ranking, translation, life-unit conversion and scenario comparison. `docs/TOOL_CONTRACTS.md` lists each tool and REST route. The REST OpenAPI contract is available at `/openapi.json` and is checked in at `verification/openapi.json`.
+
+Tests use deterministic HTTP fixtures and exercise real MCP tool serialization. The separate smoke script proves the actual HTTP mount and protocol negotiation. See `verification/README.md`, `verification/MCP_SMOKE.md`, `docs/DATA_SOURCES.md`, and `docs/JUDGE_DEMO.md`.
+
+## Submission status
+
+Source is published at [KongkouKK/life-exchange-rate-mcp](https://github.com/KongkouKK/life-exchange-rate-mcp). The application passed 148 local tests and the hosted checks described above. Deployment is on the free Vercel Hobby plan, within its personal/noncommercial terms and usage quotas. No EIA key is configured: energy demonstration results use the explicitly labeled synthetic scenario fallback, and no live EIA verification is claimed.
+
+Contest submission remains separate from deployment. `submission.json` and `SUBMISSION.md` still require final submission review; `RIGHTS.md` is a draft requiring the submitter's declarations. No official contest pull request has been submitted.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/app.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/app.py
new file mode 100644
index 0000000..cde2118
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/app.py
@@ -0,0 +1,6 @@
+"""Vercel's supported FastAPI entrypoint; also runnable with uvicorn app:app."""
+from pathlib import Path
+import sys
+
+sys.path.insert(0, str(Path(__file__).resolve().parent / "source"))
+from life_exchange_rate.main import app
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ARCHITECTURE.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..e98c9ed
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ARCHITECTURE.md
@@ -0,0 +1,30 @@
+# Life Exchange Rate MCP — v0.2 architecture
+
+## Product thesis
+
+Every macro headline has a personal exchange rate. The service converts an observed macro event into an auditable estimate of what it means in the user's own budget, work time, and everyday units.
+
+## Flow
+
+`official headlines / market data` → `structured MacroEvent` → `significance + relevance ranking` → `deterministic impact engine` → `MCP tools` → `Agent explanation`
+
+The Agent never owns the core arithmetic.
+
+## Separation of concerns
+
+- **MacroHeadline**: title, publisher, time, URL, jurisdiction, event-type hint. It explicitly requires quantification.
+- **MacroEvent**: numeric old/new values, observation window, units, currencies/jurisdiction, evidence, provenance.
+- **LifeProfile**: only the exposures needed for the selected scenario.
+- **EventScore**: transparent market-significance and user-relevance scores.
+- **ImpactResult**: money impact + work/life units + warnings + calculation trace.
+
+## Safety boundary
+
+A Fed rate move is not directly applied to a SEK mortgage. Currency/jurisdiction scope is checked before rate-impact calculation. Cross-border spillovers are outside the MVP.
+
+
+## Adapters and server lifetime
+
+FX, ECB policy-rate and EIA energy adapters normalize observations into the same `MacroEvent` and support injected HTTP transports for deterministic tests. The service layer owns provider selection. Policy/energy share date validation and fixture handling. Source dates remain separate from retrieval timestamps. FX anomaly statistics exclude the selected event from preceding equal-calendar-horizon reference returns.
+
+FastAPI and MCP share services and models. The mounted MCP application runs its session manager inside the parent lifespan. The SDK manager is single-use; tests share one lifespan per app instance. The canonical endpoint is `/mcp/`. Public deployment hosts are explicitly allowed from `PUBLIC_BASE_URL`.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DATA_SOURCES.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DATA_SOURCES.md
new file mode 100644
index 0000000..0b40bb8
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DATA_SOURCES.md
@@ -0,0 +1,113 @@
+# Structured sources and offline judging
+
+Headlines trigger attention. Only structured observations or explicitly labeled
+synthetic scenarios supply numeric event values. Adapters return the same
+`MacroEvent` model, so sources remain replaceable without changing the arithmetic.
+
+## Frankfurter with ECB provider filtering
+
+- Purpose: current and historical FX reference rates; no key.
+- Provider query explicitly selects ECB reference rates.
+- Observation dates, the actual comparison window, retrieval time, source URL,
+ and the transparent anomaly method travel with the event.
+- Reference: [Frankfurter ECB provider](https://frankfurter.dev/providers/ecb/).
+
+## European Central Bank policy rate
+
+- Adapter: `PolicyRateProvider.policy_rate_move(lookback_days=30, mode="live_or_fixture")`.
+- Source: [ECB deposit facility daily series](https://data.ecb.europa.eu/data/datasets/FM/FM.D.U2.EUR.4F.KR.DFR.LEV),
+ series `FM.D.U2.EUR.4F.KR.DFR.LEV`.
+- API: `https://data-api.ecb.europa.eu/service/data/FM/D.U2.EUR.4F.KR.DFR.LEV`.
+ No authentication. Requests use `format=csvdata`, `startPeriod`, and `endPeriod`,
+ as documented in [ECB API data examples](https://data.ecb.europa.eu/help/data-examples).
+- Use the `D` daily levels; the `B` raw date-of-change series is sparse and does
+ not provide daily comparison endpoints. Neither a sparse old change nor a news
+ announcement is substituted for the requested observation window.
+- Unit: percent per annum (`PCPA`); model unit `percent`. A move is
+ `new − old` percentage points, or `100 × (new − old)` basis points.
+ `change_pct` is deliberately null: a relative percentage is misleading when
+ the old policy rate is zero or negative.
+- Scope: euro area, affected currency EUR. The adapter does not apply this rate
+ to a SEK mortgage or claim it measures every country's policy rate.
+
+## U.S. Energy Information Administration: Brent
+
+- Adapter: `EnergyProvider.oil_move(lookback_days=7, mode="live_or_fixture")`.
+- Source: [Europe Brent spot-price history](https://www.eia.gov/dnav/pet/hist/RBRTED.htm).
+- API: `https://api.eia.gov/v2/petroleum/pri/spt/data/`, `frequency=daily`,
+ `data[0]=value`, `facets[series][]=RBRTE`. Legacy series ID: `PET.RBRTE.D`.
+- [EIA API v2 documentation](https://www.eia.gov/opendata/documentation.php)
+ specifies API-key authentication, date and facet filters, sorting, string
+ numeric values, and a 5,000-row JSON limit. This bounded single-series query
+ requests at most 394 calendar days, sorted by period, and rejects a response
+ whose reported total differs from the returned row count.
+- Live access needs an `EIA_API_KEY` environment variable (or an explicitly
+ injected key). Register through [EIA Open Data](https://www.eia.gov/opendata/).
+ Keys stay out of event metadata, source URLs, remote-payload echoes, and
+ returned error messages. Error messages are locally controlled. Do not enable
+ request-URL logging for keyed upstream calls in a deployment.
+- Unit: US dollars per barrel. This is a crude-oil benchmark. Household fuel
+ prices and pass-through remain separately supplied scenario inputs.
+
+## Date selection and validation
+
+Both new adapters accept integer lookbacks of 1–365 calendar days. They request
+the lookback plus 28 days of padding. The newest published observation anchors
+the comparison; the baseline is the last observation on or before that date
+minus the requested lookback. Padding is never silently used as the baseline.
+The baseline must be no more than 7 days before the target; the newest
+observation must be no more than 14 days before the requested as-of date.
+These tolerances handle weekends, holidays, and publication lag without
+pretending that stale values are current. Actual dates, lag, target start, and
+actual window length are explicit metadata.
+
+Missing fields, unexpected series or units, missing/non-finite numbers,
+duplicate dates, future observations, an absent baseline, and stale results
+fail the live request. Brent prices must be positive for this percentage-move
+adapter. Valid zero or negative policy rates are preserved. No live missing
+observations are interpolated. UTC midnight encodes a date-only observation;
+it is not claimed to be the publication time. `provenance.retrieved_at` is the
+separate actual retrieval time.
+
+## Fixture and fallback contract
+
+| Mode | Behavior |
+| --- | --- |
+| `live` | Fetch official structured data; fail with a sanitized error if unavailable or invalid. |
+| `fixture` | Make no network request; use the fixed synthetic judging scenario. |
+| `live_or_fixture` | Try live, then return a visibly labeled synthetic scenario if the provider or key is unavailable. |
+
+The JSON files under `source/life_exchange_rate/providers/data/` declare fixed
+step scenarios, anchored to **2025-12-31**, covering all accepted lookbacks.
+Policy levels are 3.00 then 3.25; Brent levels are 80 then 92. These are test
+inputs, **not ECB or EIA historical observations**. Expanding the declared step
+scenario creates deterministic daily test data only. It is never applied to
+live gaps. Different requested windows may correctly produce an unchanged
+synthetic value.
+
+Fixture titles start with `Synthetic scenario`; `confidence=scenario`,
+`source_type=synthetic_fixture`, and `metadata.synthetic=true`. They have no
+official evidence/source URL. `fallback_used` and a sanitized `fallback_reason`
+distinguish automatic fallback from explicitly requested fixtures. Original
+fixture dates stay fixed; retrieval time and the attempted live as-of date are
+separate. Consumers must display the fixture label and must not present these
+events as current market data.
+
+## Official RSS headline triggers
+
+- [Federal Reserve monetary-policy RSS](https://www.federalreserve.gov/feeds/press_monetary.xml).
+- [European Central Bank press RSS](https://www.ecb.europa.eu/rss/press.html).
+- Each headline has `requires_quantification=true`. A headline may direct an
+ agent to a compatible structured series; its text never supplies the rate,
+ price, date-window move, wage, or household pass-through assumption.
+
+## Source priority
+
+1. Official structured statistical or central-bank observation.
+2. Structured intermediary that identifies the official provider.
+3. Explicit synthetic fixture for reproducible offline judging.
+4. Explicit user-supplied event/scenario values.
+
+Provider documentation and series definitions checked on 2026-09-14. Offline
+tests use synthetic API-shaped responses; a passing mocked test is not a claim
+that a keyed EIA live request was performed.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEMO.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEMO.md
new file mode 100644
index 0000000..b009d37
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEMO.md
@@ -0,0 +1,27 @@
+# Demo script
+
+Use the included sample profile: a SEK-based user with a Tokyo travel budget, mortgage, savings, fuel use, and prices for beer/coffee/lunch.
+
+## Demo 1 — Yen move
+
+Prompt to the agent:
+
+> The yen strengthened sharply. I live in Sweden and have 20,000 SEK set aside for Tokyo. What changed for me?
+
+Expected flow:
+
+`get_macro_events` or `get_live_fx_event` → `translate_event_to_life` → explanation.
+
+Show: extra SEK required to restore the old JPY purchasing power, work-hours equivalent, and everyday-unit equivalents.
+
+## Demo 2 — Rate move
+
+> If the policy rate rises by 25 bp, what might that mean for my mortgage and savings?
+
+Show: scenario-labelled pass-through, payment change, savings offset, warnings.
+
+## Demo 3 — Oil spike
+
+> Brent is up 15%. What could that mean for my monthly driving cost?
+
+Show: scenario range rather than a false one-to-one forecast.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEPLOY_FREE.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEPLOY_FREE.md
new file mode 100644
index 0000000..5d546e9
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/DEPLOY_FREE.md
@@ -0,0 +1,26 @@
+# Free deployment preparation: Vercel Hobby
+
+Prepared for a personal, noncommercial hackathon demonstration. Use the Hobby plan only; do not enable a Pro trial, paid integrations or extra paid resources. No model API, database, GPU or EIA key is required to run the judging demo. Deployment has not happened yet.
+
+## Account and deployment steps
+
+1. Sign in to GitHub as KongkouKK. Publish this complete source project to the chosen public repository after reviewing the contents.
+2. Create a Vercel personal Hobby account using the official signup flow. The user completes account terms and authorizes GitHub access for this project.
+3. Import the actual project repository into Vercel. Keep the repository root as the root directory. The app uses the FastAPI framework, root app.py and the existing pyproject.toml/uv.lock.
+4. Keep automatic system environment-variable exposure enabled. GitHub deployments supply VERCEL_GIT_COMMIT_SHA; the app returns that actual source revision. The public origin comes from VERCEL_PROJECT_PRODUCTION_URL, or an explicitly configured PUBLIC_BASE_URL.
+5. Confirm the production deployment is publicly callable without Vercel login. Leave protected previews private; review uses only the production URL.
+6. Verify production health/proof against the current official validator, which uses a 60-second request timeout and does not follow redirects or retry. Our additional hosted smoke check uses a stricter 10-second target. Then verify the deterministic REST call and real MCP client against /mcp/. Record the actual production URL and SHA before filling submission.json.
+
+## Constraints
+
+Hobby is free for personal, noncommercial projects within its quotas. Quota exhaustion can make the service unavailable until limits reset; it does not authorize a paid upgrade. A function request is configured for at most 30 seconds. We use stateless MCP JSON responses and do not require long-lived sessions. Vercel currently supports FastAPI lifespan startup/shutdown; shutdown cleanup has a 500 ms platform budget. Actual hosted MCP behavior must still be tested.
+
+Render Free remains an alternative, but it sleeps after 15 idle minutes and may take about one minute to wake. That leaves little margin within X-Agent's current 60-second request timeout. Vercel Hobby is the default for this submission; actual cold-start behavior still requires hosted verification.
+
+Sources checked 2026-09-15:
+- https://vercel.com/docs/frameworks/backend/fastapi
+- https://vercel.com/docs/functions/runtimes/python
+- https://vercel.com/docs/plans/hobby
+- https://vercel.com/docs/environment-variables/system-environment-variables
+- https://render.com/docs/free
+- https://github.com/xagentAI/xagt-plugin/blob/main/scripts/validate-submission.mjs
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/EVAL_CASES.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/EVAL_CASES.md
new file mode 100644
index 0000000..d49d326
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/EVAL_CASES.md
@@ -0,0 +1,14 @@
+# Eval / edge-case matrix
+
+These cases should remain true when the Agent layer is added.
+
+| Case | Expected behavior |
+| --- | --- |
+| SEK user + JPY travel budget + SEK/JPY shock | Quantify travel purchasing-power impact. |
+| SEK user + USD Fed rate move + SEK mortgage | Do **not** mechanically apply the Fed move to the SEK mortgage. Explain that cross-border spillovers are outside the MVP. |
+| FX shock but no travel budget | Mark relevant but unquantified; request the missing exposure. |
+| FX pair does not match travel target currency | Do not produce a travel-cost number. |
+| Oil shock but no fuel-use profile | Return an unquantified result, not an invented commute estimate. |
+| Policy-rate event with matching currency + mortgage | Return a scenario estimate with explicit pass-through assumption. |
+| News headline contains “rate hike” but no numeric observation | Keep it as a headline trigger; do not invent basis points. |
+| Same event/profile/assumptions repeated | Deterministic numeric output should match. |
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/FX_ANOMALY.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/FX_ANOMALY.md
new file mode 100644
index 0000000..330db0c
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/FX_ANOMALY.md
@@ -0,0 +1,100 @@
+# Live FX observation and anomaly method
+
+The live adapter reads structured daily ECB reference rates through the
+[Frankfurter v2 ECB provider route](https://frankfurter.dev/providers/ecb/).
+The [official v2 documentation](https://frankfurter.dev/) was checked on
+2026-09-14: provider routes support the same `base`, `quotes`, `from`, and `to`
+parameters as `/v2/rates`. The dedicated route avoids the default blend of
+multiple providers. There is no API key. These are reference observations,
+not intraday prices or executable travel-money quotes.
+
+## Selecting the observed move
+
+`FrankfurterProvider.fx_move(base, quote, lookback_days=7)` validates inputs
+before any HTTP request, including when called directly or through MCP.
+Currencies are distinct three-letter ASCII codes (normalized to uppercase);
+the calendar lookback is an integer from 2 through 90. Unsupported currency
+codes are rejected by the upstream provider.
+
+1. Query the UTC current date back by `2 * lookback_days + 380` calendar days.
+ The additional history supports estimation; it is not the event window.
+2. Select the latest returned observation date `new_date`.
+3. Set the requested anchor to `new_date - lookback_days`.
+4. Select the most recent observation on or before that anchor. Weekends and
+ holidays can extend the actual event window. Never substitute the first
+ observation in the historical query or shorten an unsupported window.
+ Reject an anchor more than seven calendar days behind its requested date.
+5. Keep `old_value`, `new_value`, and ordinary percentage change
+ `100 * (new_value / old_value - 1)` for the deterministic impact engine.
+
+For example, a latest Friday observation with a five-day requested lookback
+targets Sunday. The previous Friday becomes the old observation, and both the
+actual event and its volatility comparison use a seven-calendar-day horizon.
+The requested dates, actual dates, anchor gap, observation age, retrieval time,
+and exact query URL are preserved. Source dates are represented as midnight
+UTC; these timestamps are not claims about the source's publication time.
+
+## Volatility comparison
+
+Let `H = new_date - old_date` in calendar days and `r_event = ln(new/old)`.
+Construct historical log returns `r_t = ln(rate_t / rate_(t-H))` only where:
+
+- Both exact calendar endpoints were actually observed; missing rates are
+ neither interpolated nor forward-filled.
+- The reference endpoint `t` is on or before `old_date`, so the selected event
+ never enters its own baseline.
+
+Use the latest at most 252 valid reference returns. With at least 30 samples
+and sample standard deviation greater than `1e-12`, calculate:
+
+```text
+z = (r_event - mean(reference_returns)) / sample_standard_deviation(reference_returns)
+sample_standard_deviation uses denominator n - 1
+market_significance = min(100, abs(z) * 25), rounded to one decimal
+```
+
+This is a descriptive rolling score, not a forecast, normal-distribution tail
+probability, or trading signal. Reference windows can overlap each other and
+therefore are not independent samples. A large absolute score means the
+observed move differs from the preceding equal-horizon returns, including
+their mean; it does not directly measure its impact on the user's budget.
+
+`metadata.anomaly` exposes the method, formula, effective horizon, sample count,
+minimum/maximum count, reference date span, event return, mean, sample standard
+deviation, overlap/missing-endpoint policies, signed score, and fallback reason.
+`metadata.z_score` is present only when a finite estimate is available.
+
+With fewer than 30 observations, the score is unavailable with reason
+`insufficient_history`. Zero or negligible volatility produces
+`zero_or_negligible_reference_volatility`, even if the event is large. In
+either case the radar explicitly uses its existing
+`fallback_fx_percent_move` method, `min(100, abs(change_pct) * 18)`, rather than
+inventing a z-score. The available observations still determine the impact.
+
+## Data quality and replaceability
+
+The adapter rejects malformed dates, dates outside the accepted query range, missing or
+mismatched currencies, nonpositive/nonfinite rates, malformed rows, conflicting
+duplicates, insufficient event endpoints, and nonfinite percentage changes.
+Identical same-date duplicates are collapsed and counted. No headline or LLM
+supplies a numeric value. An observation older than seven days gets an explicit
+provenance note; the exact age is always reported, including weekends. A latest
+observation more than 14 calendar days old is rejected. A baseline more than
+seven calendar days before the requested anchor is also rejected, so a gap in
+the feed cannot silently turn a one-week request into a months-long event.
+
+Frankfurter may backfill a weekend or holiday query start to the previous
+reference observation. For example, the live request starting 2025-08-16
+returned an observation dated 2025-08-15. The adapter retains up to seven
+calendar days of this start padding and discloses `history_start_date` and
+`pre_query_observation_count` separately from the requested dates. Earlier rows,
+or any row after the requested end date, still cause rejection.
+
+The constructor accepts a replacement `base_url`, an optional HTTP `transport`,
+and an optional timezone-aware `now` callable. Deterministic tests use
+`httpx.MockTransport` and a fixed UTC clock. This preserves the async provider
+interface while supporting fixture replay and network-independent verification.
+`tests/test_frankfurter.py` covers date selection, independently recalculated
+scores, exclusion of the event from the baseline, missing history/volatility,
+invalid inputs and responses, duplicate handling, stale latest observations,
+excessive anchor gaps, and HTTP failures.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/JUDGE_DEMO.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/JUDGE_DEMO.md
new file mode 100644
index 0000000..f6845c5
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/JUDGE_DEMO.md
@@ -0,0 +1,23 @@
+# 90-second judging demo
+
+## 0–15 seconds: the problem
+
+“Macro headlines are everywhere. What changes in my own budget?” Explain that headline text triggers attention while independent structured observations supply all arithmetic.
+
+## 15–35 seconds: the radar
+
+Run `uv run --frozen python -m life_exchange_rate.demo` or fetch `/v1/radar/demo`. Show the explicit sample profile, the three **synthetic** events, and separate significance/relevance scores. The Japan travel budget makes the SEK/JPY event relevant.
+
+For a current-data demo use `uv run --frozen python verification/smoke_mcp.py --live-fx`. Open `verification/end-to-end-demo.json`: the selected event contains actual ECB-filtered FX observations, dates, query URL, and historical anomaly statistics. The separately fetched Fed headline is unrelated context used to prove the headline boundary; do not claim it caused the FX move. Other fixture cards remain labeled synthetic.
+
+## 35–65 seconds: the impact
+
+Select the SEK/JPY event and call `translate_event_to_life`, then `convert_to_life_units`. Show the cost of restoring the same Japan trip purchasing power, work hours and coffee/lunch equivalents. In the fixed offline scenario: **1,739.13 SEK = 8.70 work hours = 38.65 coffees = 12.42 lunches**. In live mode, read the captured computed amounts instead; never reuse the synthetic numbers as a live result.
+
+## 65–80 seconds: inspect the calculation
+
+Show old/new rates and source timestamps, user-supplied travel budget/income/prices, the formula trace, and the explicit FX fee exclusion. For rate/fuel examples show pass-through assumptions and currency scope. A EUR policy event does not directly price a SEK mortgage.
+
+## 80–90 seconds: why MCP
+
+“Any agent can call the same nine tools and explain the returned numbers without inventing market data or rebuilding the calculation logic.” Show recorded current/legacy protocol smoke results and the successful rejection of a headline-only translation request.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/PRODUCT_SPEC.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/PRODUCT_SPEC.md
new file mode 100644
index 0000000..b9436b0
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/PRODUCT_SPEC.md
@@ -0,0 +1,36 @@
+# Life Exchange Rate — product spec v0.2
+
+## Product thesis
+
+**Every headline has a price. See yours.**
+
+Life Exchange Rate is not a macro-news summarizer. It is a personal macro-impact engine: it detects or receives a quantified macro event, checks whether the user is exposed to it, and converts the impact into money, work time, and user-defined everyday units.
+
+## The key UX distinction
+
+A **headline** is a trigger, not a number. Example: “Fed issues FOMC statement.” The system must not infer a 25 bp move from the title. It retrieves/receives a structured numeric event separately, then performs the life-impact calculation.
+
+This avoids the most dangerous demo failure: an LLM inventing market numbers from news text.
+
+## MVP user flow
+
+1. User supplies a minimal life profile: home currency/country, net income, work hours, optional travel budget/target currency, mortgage/savings, fuel use, and optional life-unit prices.
+2. Macro Event Radar retrieves structured market events and official central-bank headlines.
+3. Events are ranked by **market significance** and **user relevance**.
+4. The user or Agent selects one event.
+5. `translate_event_to_life` returns a deterministic impact, assumptions, warnings, and calculation trace.
+6. The Agent explains the result in plain language without changing the numbers.
+
+## MVP event categories
+
+- FX movement → travel purchasing power.
+- Policy-rate change → mortgage/savings scenario, only when currency exposure matches.
+- Brent/oil movement → driving-cost scenario with explicit pass-through assumption.
+
+## Non-goals for v0.2
+
+- Personalized investment advice.
+- Predicting future FX/rates/oil.
+- Inferring a numerical policy move solely from a headline.
+- Modeling cross-border monetary-policy spillovers.
+- Pretending crude oil maps one-for-one to pump prices.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ROADMAP_18_DAYS.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ROADMAP_18_DAYS.md
new file mode 100644
index 0000000..0ae4ca4
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/ROADMAP_18_DAYS.md
@@ -0,0 +1,10 @@
+# 18-day build plan
+
+- Days 1–3: deterministic impact engine, profile schema, FastAPI, MCP tool surface, reproducible demo fixtures.
+- Days 4–6: live FX provider, caching, error handling, source attribution, API contract tests.
+- Days 7–9: FRED + energy provider adapters and official central-bank headline ingestion.
+- Days 10–11: event ranking by relevance and abnormality; evidence bundle per event.
+- Days 12–13: Agent wrapper and polished demo conversations.
+- Days 14–15: deployment, `/health`, `/.well-known/xagent-verification.json`, logging, rate limits.
+- Days 16–17: clean-room reproducibility test, security/data disclosure, submission package.
+- Day 18: final demo recording, commit freeze, PR submission.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/TOOL_CONTRACTS.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/TOOL_CONTRACTS.md
new file mode 100644
index 0000000..87b27e5
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/docs/TOOL_CONTRACTS.md
@@ -0,0 +1,29 @@
+# MCP and REST contracts
+
+Canonical Streamable HTTP endpoint: `/mcp/`. MCP Python SDK v2 is retained; installed dependency versions are pinned in `uv.lock`.
+
+| MCP tool | REST endpoint | Inputs / behavior |
+| --- | --- | --- |
+| `get_macro_events` | GET `/v1/events/demo` | `mode=demo`; three fixed, explicitly synthetic scenarios. |
+| `get_official_macro_headlines` | GET `/v1/headlines/official` | `source=fed|ecb`, `limit=1..50`; headline triggers only. |
+| `get_live_fx_event` | GET `/v1/events/fx` | MCP `base_currency`, `quote_currency`; REST `base`, `quote`; distinct three-letter codes, `lookback_days=2..90` (default 7). Live official reference observations plus transparent anomaly metadata. |
+| `get_policy_rate_event` | GET `/v1/events/policy-rate` | ECB euro-area deposit rate; `lookback_days=2..365` (default 30), `mode=live|fixture|live_or_fixture`. |
+| `get_energy_event` | GET `/v1/events/energy` | EIA Brent daily spot; `lookback_days=2..365` (default 7), same modes. |
+| `rank_events_for_user` | POST `/v1/radar/rank` | Structured `events`, `profile`; returns significance, relevance, method and reasons. |
+| `translate_event_to_life` | POST `/v1/translate` | Structured `event`, `profile`, optional `assumptions`; deterministic impact with provenance and trace. |
+| `convert_to_life_units` | POST `/v1/convert` | `amount`, `currency`, `profile`; requires home currency, uses declared income and prices. |
+| `compare_event_scenarios` | POST `/v1/scenarios` | MCP multipliers (default 0.5, 1.5) plus baseline 1.0; REST named `scenarios`; optional `assumptions`. All outputs are scenarios, never forecasts. |
+
+## Numeric boundary
+
+Events require old/new numeric values, event type, unit and identifiers. FX/oil rates must be positive and finite; `change_pct`, when supplied, must agree with the derived change within 0.0001 percentage points. Missing FX/oil percentages are derived from old/new values. Policy rate units are `percent`; explicit matching `affected_currencies` are required to calculate rate impact. Timestamps include a timezone. Headline-only objects are rejected by validation.
+
+`ImpactAssumptions` defaults are explicitly declared illustrative choices: `interest_rate_pass_through=0.5`, `oil_to_fuel_pass_through=0.25`, each bounded from 0 to 1. Both REST and MCP callers can override them. Life-unit prices in another currency are excluded; no FX conversion or local price is invented.
+
+Money carries a signed direction; work and life-unit equivalents use its magnitude. FX travel has a one-off horizon. Mortgage/savings and fuel outputs have a monthly scenario horizon and must not be added to one-off travel costs without a stated period.
+
+## Provider modes and errors
+
+`fixture` makes no request; `live` returns only observed data or an error; `live_or_fixture` may return synthetic data with `metadata.fallback_used` and a sanitized reason. Synthetic events retain fixed source dates, while adapter retrieval timestamps record the actual read. They never claim an official evidence URL. EIA keys remain in environment variables and are omitted from output URLs/errors.
+
+REST invalid inputs return 422; upstream failures return sanitized 502 responses. MCP returns tool errors for invalid input or unavailable live data. Clients must check `is_error` and must not replace unavailable observations with invented values.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/pyproject.toml b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/pyproject.toml
new file mode 100644
index 0000000..b98c630
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/pyproject.toml
@@ -0,0 +1,30 @@
+[project]
+name = "life-exchange-rate-mcp"
+version = "0.2.0"
+description = "Translate macroeconomic events into concrete personal-life impacts for AI agents."
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.128,<1",
+ "httpx>=0.28,<1",
+ "mcp[cli]>=2.0.0,<3",
+ "pydantic>=2.13,<3",
+ "uvicorn[standard]>=0.48,<1",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=9,<10",
+ "pytest-asyncio>=1.2,<2",
+]
+
+[tool.pytest.ini_options]
+pythonpath = ["source"]
+testpaths = ["tests"]
+addopts = "-q"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["source/life_exchange_rate"]
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/__init__.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/__init__.py
new file mode 100644
index 0000000..d3ec452
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.2.0"
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/calculations.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/calculations.py
new file mode 100644
index 0000000..6d41d00
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/calculations.py
@@ -0,0 +1,360 @@
+from __future__ import annotations
+
+from math import expm1, log1p
+
+from .models import (
+ CalculationStep,
+ Confidence,
+ EventType,
+ ImpactDirection,
+ ImpactResult,
+ LifeProfile,
+ LifeUnitConversion,
+ MacroEvent,
+ TranslateRequest,
+)
+
+
+def _round(value: float, digits: int = 2) -> float:
+ return round(value, digits)
+
+
+def _direction(delta: float | None) -> ImpactDirection:
+ if delta is None:
+ return ImpactDirection.UNQUANTIFIED
+ if abs(delta) < 1e-9:
+ return ImpactDirection.NEUTRAL
+ return ImpactDirection.COST_INCREASE if delta > 0 else ImpactDirection.COST_DECREASE
+
+
+def convert_amount_to_life_units(amount_home: float, profile: LifeProfile) -> tuple[float, list[LifeUnitConversion]]:
+ work_hours = amount_home / profile.net_hourly_income
+ conversions: list[LifeUnitConversion] = []
+ for item in profile.life_units:
+ if item.currency != profile.home_currency:
+ continue
+ conversions.append(
+ LifeUnitConversion(
+ unit=item.name,
+ quantity=_round(amount_home / item.price),
+ currency=item.currency,
+ unit_price=item.price,
+ )
+ )
+ return _round(work_hours), conversions
+
+
+def _payment(principal: float, annual_rate_pct: float, years: float) -> float:
+ months = int(round(years * 12))
+ if months <= 0:
+ raise ValueError("remaining_years must imply at least one month")
+ monthly_rate = annual_rate_pct / 100 / 12
+ if monthly_rate == 0:
+ return principal / months
+ return principal * monthly_rate / -expm1(-months * log1p(monthly_rate))
+
+
+def _fx_impact(event: MacroEvent, profile: LifeProfile) -> ImpactResult:
+ if not event.base_currency or not event.quote_currency:
+ raise ValueError("FX events require base_currency and quote_currency")
+ if event.old_value <= 0 or event.new_value <= 0:
+ raise ValueError("FX rates must be positive")
+ if profile.travel_budget_home is None:
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ home_currency=profile.home_currency,
+ explanation="The exchange-rate move is relevant, but no travel budget was supplied, so no personal cost is calculated.",
+ confidence=event.confidence,
+ warnings=["Add travel_budget_home to quantify the impact."],
+ )
+
+ old_rate, new_rate = event.old_value, event.new_value
+ if event.base_currency == profile.home_currency:
+ target_currency = event.quote_currency
+ old_target = profile.travel_budget_home * event.old_value
+ new_target = profile.travel_budget_home * event.new_value
+ restore_budget = profile.travel_budget_home * event.old_value / event.new_value
+ elif event.quote_currency == profile.home_currency:
+ target_currency = event.base_currency
+ old_rate = 1 / event.old_value
+ new_rate = 1 / event.new_value
+ old_target = profile.travel_budget_home * old_rate
+ new_target = profile.travel_budget_home * new_rate
+ restore_budget = profile.travel_budget_home * old_rate / new_rate
+ else:
+ raise ValueError("FX event must include the profile home currency")
+
+ if profile.travel_target_currency and target_currency != profile.travel_target_currency:
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ home_currency=profile.home_currency,
+ explanation=(
+ f"This FX move involves {target_currency}, while the supplied travel plan targets "
+ f"{profile.travel_target_currency}; no travel impact is quantified."
+ ),
+ confidence=event.confidence,
+ warnings=["Use an FX pair that includes both the home currency and the travel target currency."],
+ )
+
+ extra_home = restore_budget - profile.travel_budget_home
+ work_hours, units = convert_amount_to_life_units(abs(extra_home), profile)
+ more_or_less = "more" if extra_home > 0 else "less"
+ explanation = (
+ f"To preserve the same {target_currency} purchasing power for the trip, the user would need "
+ f"about {abs(extra_home):.2f} {profile.home_currency} {more_or_less} than before."
+ )
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ direct_effect_home=_round(extra_home),
+ home_currency=profile.home_currency,
+ direction=_direction(extra_home),
+ impact_horizon="one_off_travel_budget",
+ work_hours_equivalent=work_hours,
+ life_units=units,
+ explanation=explanation,
+ confidence=event.confidence,
+ assumptions=["Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."],
+ calculation_trace=[
+ CalculationStep(
+ label="old destination purchasing power",
+ formula="travel_budget_home × old_fx_rate",
+ inputs={"travel_budget_home": profile.travel_budget_home, "old_fx_rate": old_rate},
+ result=_round(old_target),
+ ),
+ CalculationStep(
+ label="new destination purchasing power",
+ formula="travel_budget_home × new_fx_rate",
+ inputs={"travel_budget_home": profile.travel_budget_home, "new_fx_rate": new_rate},
+ result=_round(new_target),
+ ),
+ CalculationStep(
+ label="budget required to restore old purchasing power",
+ formula="travel_budget_home × old_fx_rate / new_fx_rate",
+ inputs={
+ "travel_budget_home": profile.travel_budget_home,
+ "old_fx_rate": old_rate,
+ "new_fx_rate": new_rate,
+ },
+ result=_round(restore_budget),
+ ),
+ CalculationStep(
+ label="personal impact",
+ formula="restore_budget - travel_budget_home",
+ inputs={"restore_budget": _round(restore_budget), "travel_budget_home": profile.travel_budget_home},
+ result=_round(extra_home),
+ ),
+ ],
+ details={
+ "old_target_purchasing_power": _round(old_target),
+ "new_target_purchasing_power": _round(new_target),
+ "target_currency": target_currency,
+ "travel_budget_home": profile.travel_budget_home,
+ },
+ )
+
+
+def _interest_impact(request: TranslateRequest) -> ImpactResult:
+ event, profile, assumptions = request.event, request.profile, request.assumptions
+ delta_points = event.new_value - event.old_value
+ pass_through = assumptions.interest_rate_pass_through
+ warnings = [
+ "Policy-rate changes do not mechanically pass through one-for-one to household borrowing or deposit rates.",
+ "This is a scenario estimate, not a forecast of the user's bank rate.",
+ ]
+
+ exposure_currency = profile.home_currency
+ mortgage_eligible = bool(profile.mortgage and profile.mortgage_currency == profile.home_currency)
+ if profile.mortgage and not mortgage_eligible:
+ warnings.append("Foreign-currency mortgage excluded: conversion into home currency requires a separate FX model.")
+ if not event.affected_currencies or exposure_currency not in event.affected_currencies:
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ home_currency=profile.home_currency,
+ explanation=(
+ f"The event is scoped to {', '.join(event.affected_currencies)}, while the user's declared rate exposure "
+ f"is {exposure_currency}; no direct mortgage or savings impact is calculated."
+ ),
+ confidence=Confidence.SCENARIO,
+ warnings=["Cross-border monetary-policy spillovers require a separate model and are outside the MVP.", "Explicit matching affected_currencies are required for a direct rate scenario."],
+ details={"exposure_currency": exposure_currency, "event_affected_currencies": event.affected_currencies},
+ )
+
+ details: dict[str, float | str] = {
+ "policy_rate_change_percentage_points": _round(delta_points, 4),
+ "assumed_pass_through": pass_through,
+ "exposure_currency": exposure_currency,
+ }
+ trace: list[CalculationStep] = []
+ monthly_effect = 0.0
+
+ if mortgage_eligible:
+ current = profile.mortgage.current_annual_rate_pct
+ assumed_new = max(0.0, current + delta_points * pass_through)
+ old_payment = _payment(profile.mortgage.principal, current, profile.mortgage.remaining_years)
+ new_payment = _payment(profile.mortgage.principal, assumed_new, profile.mortgage.remaining_years)
+ mortgage_delta = new_payment - old_payment
+ monthly_effect += mortgage_delta
+ details.update(
+ {
+ "mortgage_old_monthly_payment": _round(old_payment),
+ "mortgage_scenario_monthly_payment": _round(new_payment),
+ "mortgage_monthly_delta": _round(mortgage_delta),
+ "mortgage_scenario_rate_pct": _round(assumed_new, 4),
+ }
+ )
+ trace.append(
+ CalculationStep(
+ label="mortgage scenario rate",
+ formula="max(0, current_mortgage_rate + policy_rate_change × pass_through)",
+ inputs={"current_rate_pct": current, "policy_change_pp": delta_points, "pass_through": pass_through},
+ result=_round(assumed_new, 4),
+ )
+ )
+
+ if profile.savings_balance:
+ savings_delta = profile.savings_balance * (delta_points * pass_through / 100) / 12
+ monthly_effect -= savings_delta
+ details["savings_monthly_interest_delta"] = _round(savings_delta)
+ trace.append(
+ CalculationStep(
+ label="savings interest offset",
+ formula="savings_balance × policy_rate_change / 100 × pass_through / 12",
+ inputs={"savings_balance": profile.savings_balance, "policy_change_pp": delta_points, "pass_through": pass_through},
+ result=_round(savings_delta),
+ )
+ )
+
+ if not mortgage_eligible and not profile.savings_balance:
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ home_currency=profile.home_currency,
+ explanation="The rate move may matter, but the profile has no mortgage or savings exposure to quantify.",
+ confidence=Confidence.SCENARIO,
+ assumptions=[f"Interest-rate pass-through set to {pass_through:.0%}."],
+ warnings=warnings,
+ details=details,
+ )
+
+ work_hours, units = convert_amount_to_life_units(abs(monthly_effect), profile)
+ direction = "higher" if monthly_effect > 0 else "lower"
+ trace.append(
+ CalculationStep(
+ label="net monthly household impact",
+ formula="mortgage_payment_delta - savings_interest_delta",
+ inputs={
+ "mortgage_payment_delta": details.get("mortgage_monthly_delta", 0.0),
+ "savings_interest_delta": details.get("savings_monthly_interest_delta", 0.0),
+ },
+ result=_round(monthly_effect),
+ )
+ )
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ direct_effect_home=_round(monthly_effect),
+ home_currency=profile.home_currency,
+ direction=_direction(monthly_effect),
+ impact_horizon="monthly_scenario",
+ work_hours_equivalent=work_hours,
+ life_units=units,
+ explanation=f"Under the chosen pass-through assumption, monthly net household cost is about {abs(monthly_effect):.2f} {profile.home_currency} {direction}.",
+ confidence=Confidence.SCENARIO,
+ assumptions=[f"Interest-rate pass-through set to {pass_through:.0%}."],
+ warnings=warnings,
+ calculation_trace=trace,
+ details=details,
+ )
+
+
+def _oil_impact(request: TranslateRequest) -> ImpactResult:
+ event, profile, assumptions = request.event, request.profile, request.assumptions
+ if not profile.commute:
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ home_currency=profile.home_currency,
+ explanation="The oil-price move may affect transport costs, but no fuel-consumption profile was supplied.",
+ confidence=Confidence.SCENARIO,
+ warnings=["Add commute.monthly_fuel_liters and fuel_price_per_liter to quantify a scenario."],
+ )
+
+ crude_change = event.change_pct
+ if crude_change is None:
+ if event.old_value == 0:
+ raise ValueError("Cannot derive oil change from a zero old_value")
+ crude_change = (event.new_value / event.old_value - 1) * 100
+
+ pass_through = assumptions.oil_to_fuel_pass_through
+ fuel_pct_change = crude_change * pass_through
+ old_monthly = profile.commute.monthly_fuel_liters * profile.commute.fuel_price_per_liter
+ new_monthly = old_monthly * (1 + fuel_pct_change / 100)
+ delta = new_monthly - old_monthly
+ work_hours, units = convert_amount_to_life_units(abs(delta), profile)
+ return ImpactResult(
+ event_id=event.event_id,
+ headline=event.title,
+ direct_effect_home=_round(delta),
+ home_currency=profile.home_currency,
+ direction=_direction(delta),
+ impact_horizon="monthly_scenario",
+ work_hours_equivalent=work_hours,
+ life_units=units,
+ explanation=f"Under the selected pass-through scenario, monthly fuel spending changes by about {delta:.2f} {profile.home_currency}.",
+ confidence=Confidence.SCENARIO,
+ assumptions=[f"{pass_through:.0%} of the crude-oil percentage move is applied to the user's pump price as a scenario."],
+ warnings=[
+ "Crude-oil prices do not translate one-for-one into retail fuel prices.",
+ "Taxes, refining, distribution, inventories, currency moves, and timing can materially change pass-through.",
+ ],
+ calculation_trace=[
+ CalculationStep(
+ label="fuel-price scenario move",
+ formula="crude_change_pct × pass_through",
+ inputs={"crude_change_pct": _round(crude_change), "pass_through": pass_through},
+ result=_round(fuel_pct_change),
+ ),
+ CalculationStep(
+ label="baseline monthly fuel spend",
+ formula="monthly_fuel_liters × fuel_price_per_liter",
+ inputs={
+ "monthly_fuel_liters": profile.commute.monthly_fuel_liters,
+ "fuel_price_per_liter": profile.commute.fuel_price_per_liter,
+ },
+ result=_round(old_monthly),
+ ),
+ CalculationStep(
+ label="monthly impact",
+ formula="baseline_monthly_spend × scenario_fuel_change_pct / 100",
+ inputs={"baseline_monthly_spend": _round(old_monthly), "scenario_fuel_change_pct": _round(fuel_pct_change)},
+ result=_round(delta),
+ ),
+ ],
+ details={
+ "crude_change_pct": _round(crude_change),
+ "scenario_fuel_change_pct": _round(fuel_pct_change),
+ "old_monthly_fuel_cost": _round(old_monthly),
+ "scenario_monthly_fuel_cost": _round(new_monthly),
+ },
+ )
+
+
+def translate_event(request: TranslateRequest) -> ImpactResult:
+ if request.event.event_type == EventType.FX_MOVE:
+ result = _fx_impact(request.event, request.profile)
+ elif request.event.event_type == EventType.INTEREST_RATE_CHANGE:
+ result = _interest_impact(request)
+ elif request.event.event_type == EventType.OIL_MOVE:
+ result = _oil_impact(request)
+ else:
+ raise ValueError(f"Unsupported event type: {request.event.event_type}")
+ result.event_provenance = request.event.provenance
+ result.event_observed_at = request.event.observed_at
+ if request.event.provenance and request.event.provenance.source_type in ("fixture", "synthetic_fixture"):
+ result.warnings.append("This event uses a synthetic judging fixture, not a current market observation.")
+ return result
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/demo.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/demo.py
new file mode 100644
index 0000000..9e11497
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/demo.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import argparse
+import asyncio
+import json
+
+from .models import ConvertRequest, RadarRequest, TranslateRequest
+from .providers.fixtures import demo_profile
+from .service import (
+ convert_request, get_demo_events, get_live_fx_event, get_policy_rate_event,
+ rank_request, translate_request,
+)
+
+
+def sample_profile():
+ """All wages, budgets and life-unit prices are declared demonstration inputs."""
+ return demo_profile()
+
+
+async def run_demo(live: bool = False) -> dict:
+ profile = sample_profile()
+ events = (
+ [await get_live_fx_event("SEK", "JPY"), await get_policy_rate_event(mode="live")]
+ if live else get_demo_events()
+ )
+ ranked = rank_request(RadarRequest(events=events, profile=profile))
+ selected = next(event for event in events if event.base_currency == "SEK" and event.quote_currency == "JPY")
+ impact = translate_request(TranslateRequest(event=selected, profile=profile))
+ conversion = convert_request(ConvertRequest(amount=impact["direct_effect_home"], currency="SEK", profile=profile))
+ return {
+ "mode": "live_official_structured_events" if live else "synthetic_offline_fixture",
+ "profile_source": "Explicit synthetic demonstration inputs; not inferred user finances or local prices.",
+ "profile": profile.model_dump(mode="json"),
+ "radar": ranked,
+ "selected_event_id": selected.event_id,
+ "impact": impact,
+ "life_conversion": conversion,
+ "boundary": "Only structured old/new observations drive arithmetic; headlines never supply values.",
+ }
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Structured event -> radar -> JPY/SEK travel -> work/life units")
+ parser.add_argument("--live", action="store_true", help="Use live ECB/Frankfurter observations; fail if unavailable.")
+ args = parser.parse_args()
+ print(json.dumps(asyncio.run(run_demo(args.live)), ensure_ascii=False, indent=2, allow_nan=False))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/main.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/main.py
new file mode 100644
index 0000000..9980d14
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/main.py
@@ -0,0 +1,214 @@
+from __future__ import annotations
+
+import importlib.util
+import os
+from contextlib import asynccontextmanager
+from typing import Literal
+from urllib.parse import urlsplit
+
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import JSONResponse
+
+from . import __version__
+from .models import ConvertRequest, EventScore, ImpactResult, MacroEvent, MacroHeadline, RadarRequest, ScenarioRequest, TranslateRequest
+from .service import (
+ compare_scenarios,
+ convert_request,
+ get_demo_events,
+ get_demo_radar,
+ get_energy_event,
+ get_live_fx_event,
+ get_official_headlines,
+ get_policy_rate_event,
+ rank_request,
+ translate_request,
+)
+
+
+def _commit() -> str:
+ return os.getenv("VERCEL_GIT_COMMIT_SHA") or os.getenv("RENDER_GIT_COMMIT") or os.getenv("REVIEW_COMMIT", "dev-local")
+
+
+def _base_url() -> str:
+ configured = os.getenv("PUBLIC_BASE_URL")
+ if configured:
+ return configured.rstrip("/")
+ platform_host = os.getenv("VERCEL_PROJECT_PRODUCTION_URL") or os.getenv("VERCEL_URL")
+ return f"https://{platform_host}" if platform_host else "http://localhost:8000"
+
+
+def _transport_security():
+ from mcp.server.transport_security import TransportSecuritySettings
+
+ public = urlsplit(_base_url())
+ if public.scheme not in {"http", "https"} or not public.hostname or public.username or public.password:
+ raise ValueError("PUBLIC_BASE_URL must be an HTTP(S) URL without credentials")
+ public_host = f"[{public.hostname}]" if ":" in public.hostname else public.hostname
+ local_hosts = ["localhost", "127.0.0.1", "[::1]"]
+ allowed_hosts = [value for host in local_hosts for value in (host, f"{host}:*")]
+ allowed_hosts.extend([public_host, public.netloc])
+ local_origins = [value for host in local_hosts for value in (f"http://{host}", f"http://{host}:*")]
+ return TransportSecuritySettings(
+ allowed_hosts=list(dict.fromkeys(allowed_hosts)),
+ allowed_origins=list(dict.fromkeys([*local_origins, f"{public.scheme}://{public.netloc}"])),
+ )
+
+
+MCP_AVAILABLE = importlib.util.find_spec("mcp") is not None
+_mcp = None
+_mcp_app = None
+if MCP_AVAILABLE:
+ from .mcp_server import mcp as _mcp
+
+ _mcp_app = _mcp.streamable_http_app(
+ streamable_http_path="/",
+ stateless_http=True,
+ json_response=True,
+ transport_security=_transport_security(),
+ )
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # Mounted ASGI applications do not run their own lifespan.
+ if _mcp is not None:
+ async with _mcp.session_manager.run():
+ yield
+ else:
+ yield
+
+
+app = FastAPI(
+ title="Life Exchange Rate API",
+ version=__version__,
+ description="Translate macroeconomic shocks into concrete, user-specific life impacts.",
+ lifespan=lifespan,
+)
+
+if _mcp_app is not None:
+ app.mount("/mcp", _mcp_app)
+
+
+@app.exception_handler(RequestValidationError)
+async def invalid_request_handler(request, exc: RequestValidationError):
+ # Do not echo raw input: nonfinite JSON values cannot be serialized safely,
+ # and validation responses need only the field location and explanation.
+ return JSONResponse(status_code=422, content={
+ "detail": [{"loc": list(error["loc"]), "msg": error["msg"], "type": error["type"]}
+ for error in exc.errors()]
+ })
+
+
+@app.get("/health")
+def health() -> dict:
+ return {
+ "status": "ok",
+ "version": __version__,
+ "commit": _commit(),
+ "mcp": "enabled" if MCP_AVAILABLE else "dependency-not-installed",
+ }
+
+
+@app.get("/.well-known/xagent-verification.json")
+def xagent_verification() -> dict:
+ return {
+ "schemaVersion": 1,
+ "slug": os.getenv("SUBMISSION_SLUG", "kongkoukk-life-exchange-rate"),
+ "version": __version__,
+ "commit": _commit(),
+ "apiBaseUrl": _base_url(),
+ "health": f"{_base_url()}/health",
+ "openapi": f"{_base_url()}/openapi.json",
+ "mcp": f"{_base_url()}/mcp/" if MCP_AVAILABLE else None,
+ }
+
+
+@app.get("/v1/events/demo", response_model=list[MacroEvent])
+def events_demo() -> list[dict]:
+ return [event.model_dump(mode="json") for event in get_demo_events()]
+
+
+@app.get("/v1/radar/demo", response_model=list[EventScore])
+def radar_demo() -> list[dict]:
+ return get_demo_radar()
+
+
+@app.get("/v1/events/fx", response_model=MacroEvent)
+async def events_fx(
+ base: str = Query(pattern="^[A-Za-z]{3}$"),
+ quote: str = Query(pattern="^[A-Za-z]{3}$"),
+ lookback_days: int = Query(default=7, ge=2, le=90),
+) -> dict:
+ if base.upper() == quote.upper():
+ raise HTTPException(status_code=422, detail="FX base and quote currencies must differ")
+ try:
+ event = await get_live_fx_event(base=base, quote=quote, lookback_days=lookback_days)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise HTTPException(status_code=502, detail="FX provider unavailable; retry later.") from exc
+
+
+@app.get("/v1/events/policy-rate", response_model=MacroEvent)
+async def events_policy_rate(
+ lookback_days: int = Query(default=30, ge=2, le=365),
+ mode: Literal["live", "fixture", "live_or_fixture"] = "live_or_fixture",
+) -> dict:
+ try:
+ event = await get_policy_rate_event(lookback_days=lookback_days, mode=mode)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise HTTPException(status_code=502, detail="Policy-rate provider unavailable; retry later or use fixture mode.") from exc
+
+
+@app.get("/v1/events/energy", response_model=MacroEvent)
+async def events_energy(
+ lookback_days: int = Query(default=7, ge=2, le=365),
+ mode: Literal["live", "fixture", "live_or_fixture"] = "live_or_fixture",
+) -> dict:
+ try:
+ event = await get_energy_event(lookback_days=lookback_days, mode=mode)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise HTTPException(status_code=502, detail="Energy provider unavailable; retry later or use fixture mode.") from exc
+
+
+@app.get("/v1/headlines/official", response_model=list[MacroHeadline])
+async def headlines_official(
+ source: Literal["fed", "ecb"],
+ limit: int = Query(default=10, ge=1, le=50),
+) -> list[dict]:
+ try:
+ return await get_official_headlines(source=source, limit=limit)
+ except Exception as exc:
+ raise HTTPException(status_code=502, detail="Official headline provider unavailable; retry later.") from exc
+
+
+@app.post("/v1/radar/rank", response_model=list[EventScore])
+def radar_rank(request: RadarRequest) -> list[dict]:
+ return rank_request(request)
+
+
+@app.post("/v1/translate", response_model=ImpactResult)
+def translate(request: TranslateRequest) -> dict:
+ try:
+ return translate_request(request)
+ except ValueError as exc:
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
+
+
+@app.post("/v1/convert")
+def convert(request: ConvertRequest) -> dict:
+ try:
+ return convert_request(request)
+ except ValueError as exc:
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
+
+
+@app.post("/v1/scenarios")
+def scenarios(request: ScenarioRequest) -> list[dict]:
+ try:
+ return compare_scenarios(request)
+ except ValueError as exc:
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
+
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/mcp_server.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/mcp_server.py
new file mode 100644
index 0000000..b2d8e0d
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/mcp_server.py
@@ -0,0 +1,159 @@
+from __future__ import annotations
+
+from typing import Annotated, Any, Literal
+
+from mcp.server import MCPServer
+from mcp.server.mcpserver.exceptions import ToolError
+from pydantic import Field
+
+from . import __version__
+from .models import (
+ ConvertRequest,
+ ImpactAssumptions,
+ LifeProfile,
+ MacroEvent,
+ RadarRequest,
+ ScenarioInput,
+ ScenarioRequest,
+ TranslateRequest,
+)
+from .service import (
+ compare_scenarios,
+ convert_request,
+ get_demo_events,
+ get_energy_event as fetch_energy_event,
+ get_live_fx_event as fetch_live_fx_event,
+ get_official_headlines,
+ get_policy_rate_event as fetch_policy_rate_event,
+ rank_request,
+ translate_request,
+)
+
+Currency = Annotated[str, Field(pattern="^[A-Za-z]{3}$")]
+FXLookback = Annotated[int, Field(ge=2, le=90)]
+ProviderLookback = Annotated[int, Field(ge=2, le=365)]
+ProviderMode = Literal["live", "fixture", "live_or_fixture"]
+HeadlineLimit = Annotated[int, Field(ge=1, le=50)]
+PositiveMultiplier = Annotated[float, Field(gt=0, allow_inf_nan=False)]
+
+mcp = MCPServer(
+ "Life Exchange Rate",
+ version=__version__,
+ instructions=(
+ "Translate macroeconomic events into concrete personal-life impacts. "
+ "Separate headlines from quantified events; headlines alone cannot be calculated. "
+ "Prefer structured observations, identify fixtures and fallback provenance, "
+ "keep supplied prices, wages and pass-through assumptions explicit, "
+ "and never present scenario estimates as guaranteed forecasts."
+ ),
+)
+
+
+@mcp.tool()
+def get_macro_events(mode: Literal["demo"] = "demo") -> list[dict[str, Any]]:
+ """Return explicitly labeled, reproducible synthetic macro events for the judging demo."""
+ return [event.model_dump(mode="json") for event in get_demo_events()]
+
+
+@mcp.tool()
+async def get_official_macro_headlines(
+ source: Literal["fed", "ecb"] = "fed", limit: HeadlineLimit = 10,
+) -> list[dict[str, Any]]:
+ """Read official central-bank headlines as triggers only; quantify separately before calculating impact."""
+ try:
+ return await get_official_headlines(source=source, limit=limit)
+ except Exception as exc:
+ raise ToolError("Official headline provider unavailable; retry later.") from exc
+
+
+@mcp.tool()
+async def get_live_fx_event(
+ base_currency: Currency, quote_currency: Currency, lookback_days: FXLookback = 7,
+) -> dict[str, Any]:
+ """Get an observed FX move with a transparent rolling anomaly score from ECB-filtered Frankfurter data."""
+ if base_currency.upper() == quote_currency.upper():
+ raise ToolError("FX base and quote currencies must differ")
+ try:
+ event = await fetch_live_fx_event(base_currency, quote_currency, lookback_days)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise ToolError("FX provider unavailable; retry later.") from exc
+
+
+@mcp.tool()
+async def get_policy_rate_event(
+ lookback_days: ProviderLookback = 30, mode: ProviderMode = "live_or_fixture",
+) -> dict[str, Any]:
+ """Get structured ECB euro-area deposit-rate changes scoped to EUR; fixture mode is synthetic and fallback is labeled in provenance."""
+ try:
+ event = await fetch_policy_rate_event(lookback_days=lookback_days, mode=mode)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise ToolError("Policy-rate provider unavailable; retry later or use fixture mode.") from exc
+
+
+@mcp.tool()
+async def get_energy_event(
+ lookback_days: ProviderLookback = 7, mode: ProviderMode = "live_or_fixture",
+) -> dict[str, Any]:
+ """Get structured EIA Brent observations; fixture mode is reproducible and crude-to-fuel pass-through remains a scenario."""
+ try:
+ event = await fetch_energy_event(lookback_days=lookback_days, mode=mode)
+ return event.model_dump(mode="json")
+ except Exception as exc:
+ raise ToolError("Energy provider unavailable; retry later or use fixture mode.") from exc
+
+
+@mcp.tool()
+def rank_events_for_user(events: list[MacroEvent], profile: LifeProfile) -> list[dict[str, Any]]:
+ """Rank quantified events by transparent market significance and relevance to the supplied profile."""
+ return rank_request(RadarRequest(events=events, profile=profile))
+
+
+@mcp.tool()
+def translate_event_to_life(
+ event: MacroEvent, profile: LifeProfile, assumptions: ImpactAssumptions | None = None,
+) -> dict[str, Any]:
+ """Calculate money, work time and supplied life-unit equivalents; optional pass-through inputs are scenarios, not forecasts."""
+ try:
+ return translate_request(TranslateRequest(
+ event=event, profile=profile, assumptions=assumptions or ImpactAssumptions(),
+ ))
+ except ValueError as exc:
+ raise ToolError(str(exc)) from exc
+
+
+@mcp.tool()
+def convert_to_life_units(
+ amount: Annotated[float, Field(allow_inf_nan=False)], currency: Currency, profile: LifeProfile,
+) -> dict[str, Any]:
+ """Convert a home-currency amount to work time and user-supplied everyday units without inventing prices or wages."""
+ try:
+ return convert_request(ConvertRequest(amount=amount, currency=currency, profile=profile))
+ except ValueError as exc:
+ raise ToolError(str(exc)) from exc
+
+
+@mcp.tool()
+def compare_event_scenarios(
+ event: MacroEvent,
+ profile: LifeProfile,
+ mild_multiplier: PositiveMultiplier = 0.5,
+ severe_multiplier: PositiveMultiplier = 1.5,
+ assumptions: ImpactAssumptions | None = None,
+) -> list[dict[str, Any]]:
+ """Compare milder, baseline and more severe versions of an event using explicit pass-through assumptions; this is not a forecast."""
+ try:
+ request = ScenarioRequest(
+ event=event,
+ profile=profile,
+ scenarios=[
+ ScenarioInput(label="milder", multiplier=mild_multiplier),
+ ScenarioInput(label="baseline", multiplier=1.0),
+ ScenarioInput(label="severe", multiplier=severe_multiplier),
+ ],
+ assumptions=assumptions or ImpactAssumptions(),
+ )
+ return compare_scenarios(request)
+ except ValueError as exc:
+ raise ToolError(str(exc)) from exc
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/models.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/models.py
new file mode 100644
index 0000000..a301f5c
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/models.py
@@ -0,0 +1,256 @@
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from enum import Enum
+from typing import Any
+
+from pydantic import BaseModel as PydanticBaseModel, ConfigDict, Field, PositiveFloat, field_validator, model_validator
+
+
+class BaseModel(PydanticBaseModel):
+ model_config = ConfigDict(allow_inf_nan=False)
+
+ @field_validator("*", check_fields=False)
+ @classmethod
+ def timezone_required(cls, value):
+ if isinstance(value, datetime) and value.utcoffset() is None:
+ raise ValueError("Timestamps must include a timezone")
+ return value
+
+
+class EventType(str, Enum):
+ FX_MOVE = "fx_move"
+ INTEREST_RATE_CHANGE = "interest_rate_change"
+ OIL_MOVE = "oil_move"
+
+
+class Confidence(str, Enum):
+ OBSERVED = "observed"
+ SCENARIO = "scenario"
+ MIXED = "mixed"
+
+
+class ImpactDirection(str, Enum):
+ COST_INCREASE = "cost_increase"
+ COST_DECREASE = "cost_decrease"
+ NEUTRAL = "neutral"
+ UNQUANTIFIED = "unquantified"
+
+
+class Provenance(BaseModel):
+ provider: str
+ source_type: str = "official_or_structured"
+ retrieved_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
+ series_id: str | None = None
+ source_url: str | None = None
+ notes: list[str] = Field(default_factory=list)
+
+
+class MacroEvent(BaseModel):
+ event_id: str
+ event_type: EventType
+ title: str
+ source: str
+ observed_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
+ old_value: float
+ new_value: float
+ unit: str
+ change_pct: float | None = None
+ base_currency: str | None = None
+ quote_currency: str | None = None
+ affected_currencies: list[str] = Field(default_factory=list)
+ jurisdiction: str | None = None
+ evidence_url: str | None = None
+ window_start: datetime | None = None
+ window_end: datetime | None = None
+ confidence: Confidence = Confidence.OBSERVED
+ provenance: Provenance | None = None
+ metadata: dict[str, Any] = Field(default_factory=dict)
+
+ @model_validator(mode="after")
+ def validate_numeric_event(self):
+ if self.event_type == EventType.FX_MOVE:
+ if not self.base_currency or not self.quote_currency or self.base_currency == self.quote_currency:
+ raise ValueError("FX events require two distinct currencies")
+ if self.event_type in (EventType.FX_MOVE, EventType.OIL_MOVE):
+ if self.old_value <= 0 or self.new_value <= 0:
+ raise ValueError("FX rates and oil prices must be positive")
+ derived = (self.new_value / self.old_value - 1) * 100
+ if self.change_pct is not None and abs(self.change_pct - derived) > 0.0001:
+ raise ValueError("change_pct must agree with the structured old/new values")
+ self.change_pct = derived
+ if self.event_type == EventType.INTEREST_RATE_CHANGE and self.unit != "percent":
+ raise ValueError("Policy rates must be expressed in percent, not basis points")
+ if self.window_start and self.window_end and self.window_start > self.window_end:
+ raise ValueError("Observation window start must not be after its end")
+ return self
+
+ @field_validator("base_currency", "quote_currency")
+ @classmethod
+ def uppercase_currency(cls, value: str | None) -> str | None:
+ return value.upper() if value else value
+
+ @field_validator("affected_currencies")
+ @classmethod
+ def uppercase_currencies(cls, value: list[str]) -> list[str]:
+ return [item.upper() for item in value]
+
+ @field_validator("jurisdiction")
+ @classmethod
+ def uppercase_jurisdiction(cls, value: str | None) -> str | None:
+ return value.upper() if value else value
+
+
+class MacroHeadline(BaseModel):
+ headline_id: str
+ title: str
+ publisher: str
+ published_at: datetime | None = None
+ url: str
+ jurisdiction: str | None = None
+ event_type_hint: EventType | None = None
+ tags: list[str] = Field(default_factory=list)
+ requires_quantification: bool = True
+ source_type: str = "official_rss"
+
+
+class LifeUnit(BaseModel):
+ name: str
+ price: PositiveFloat
+ currency: str
+
+ @field_validator("currency")
+ @classmethod
+ def uppercase_currency(cls, value: str) -> str:
+ return value.upper()
+
+
+class MortgageProfile(BaseModel):
+ principal: PositiveFloat
+ remaining_years: PositiveFloat
+ current_annual_rate_pct: float = Field(ge=0, le=100)
+ currency: str | None = None
+
+ @field_validator("currency")
+ @classmethod
+ def uppercase_currency(cls, value: str | None) -> str | None:
+ return value.upper() if value else value
+
+
+class CommuteProfile(BaseModel):
+ monthly_fuel_liters: float = Field(ge=0)
+ fuel_price_per_liter: PositiveFloat
+
+
+class LifeProfile(BaseModel):
+ home_currency: str
+ home_country: str | None = None
+ net_monthly_income: float = Field(gt=0)
+ monthly_work_hours: float = Field(gt=0)
+ travel_budget_home: float | None = Field(default=None, ge=0)
+ travel_target_currency: str | None = None
+ mortgage: MortgageProfile | None = None
+ savings_balance: float | None = Field(default=None, ge=0)
+ commute: CommuteProfile | None = None
+ life_units: list[LifeUnit] = Field(default_factory=list)
+
+ @field_validator("home_currency", "travel_target_currency")
+ @classmethod
+ def uppercase_currency(cls, value: str | None) -> str | None:
+ return value.upper() if value else value
+
+ @field_validator("home_country")
+ @classmethod
+ def uppercase_country(cls, value: str | None) -> str | None:
+ return value.upper() if value else value
+
+ @property
+ def net_hourly_income(self) -> float:
+ return self.net_monthly_income / self.monthly_work_hours
+
+ @property
+ def mortgage_currency(self) -> str | None:
+ if not self.mortgage:
+ return None
+ return self.mortgage.currency or self.home_currency
+
+
+class ImpactAssumptions(BaseModel):
+ interest_rate_pass_through: float = Field(default=0.5, ge=0, le=1)
+ oil_to_fuel_pass_through: float = Field(default=0.25, ge=0, le=1)
+
+
+class TranslateRequest(BaseModel):
+ event: MacroEvent
+ profile: LifeProfile
+ assumptions: ImpactAssumptions = Field(default_factory=ImpactAssumptions)
+
+
+class LifeUnitConversion(BaseModel):
+ unit: str
+ quantity: float
+ currency: str
+ unit_price: float
+
+
+class CalculationStep(BaseModel):
+ label: str
+ formula: str
+ inputs: dict[str, float | str | None] = Field(default_factory=dict)
+ result: float | str | None = None
+
+
+class ImpactResult(BaseModel):
+ event_provenance: Provenance | None = None
+ event_observed_at: datetime | None = None
+ event_id: str
+ headline: str
+ direct_effect_home: float | None = None
+ home_currency: str
+ direction: ImpactDirection = ImpactDirection.UNQUANTIFIED
+ impact_horizon: str | None = None
+ work_hours_equivalent: float | None = None
+ life_units: list[LifeUnitConversion] = Field(default_factory=list)
+ explanation: str
+ confidence: Confidence
+ assumptions: list[str] = Field(default_factory=list)
+ warnings: list[str] = Field(default_factory=list)
+ calculation_trace: list[CalculationStep] = Field(default_factory=list)
+ details: dict[str, Any] = Field(default_factory=dict)
+
+
+class ConvertRequest(BaseModel):
+ amount: float
+ currency: str
+ profile: LifeProfile
+
+ @field_validator("currency")
+ @classmethod
+ def uppercase_currency(cls, value: str) -> str:
+ return value.upper()
+
+
+class ScenarioInput(BaseModel):
+ label: str
+ multiplier: float = Field(gt=0)
+
+
+class ScenarioRequest(BaseModel):
+ event: MacroEvent
+ profile: LifeProfile
+ scenarios: list[ScenarioInput]
+ assumptions: ImpactAssumptions = Field(default_factory=ImpactAssumptions)
+
+
+class EventScore(BaseModel):
+ event: MacroEvent
+ market_significance: float = Field(ge=0, le=100)
+ user_relevance: float = Field(ge=0, le=100)
+ priority_score: float = Field(ge=0, le=100)
+ significance_method: str
+ relevance_reasons: list[str] = Field(default_factory=list)
+
+
+class RadarRequest(BaseModel):
+ events: list[MacroEvent]
+ profile: LifeProfile
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/common.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/common.py
new file mode 100644
index 0000000..527d307
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/common.py
@@ -0,0 +1,114 @@
+"""Shared validation and sanitized failures for structured data adapters."""
+from __future__ import annotations
+
+import json
+import math
+from datetime import date, datetime, time, timedelta, timezone
+from pathlib import Path
+from typing import Literal
+
+import httpx
+
+ProviderMode = Literal["live", "fixture", "live_or_fixture"]
+Observation = tuple[date, float]
+
+
+class ProviderError(ValueError):
+ """Controlled local messages, never remote payloads or keyed URLs."""
+
+
+def validate_request(lookback_days: int, mode: str) -> None:
+ if isinstance(lookback_days, bool) or not isinstance(lookback_days, int) or not 1 <= lookback_days <= 365:
+ raise ValueError("lookback_days must be an integer from 1 to 365")
+ if mode not in {"live", "fixture", "live_or_fixture"}:
+ raise ValueError("mode must be live, fixture, or live_or_fixture")
+
+
+def utc_now() -> datetime:
+ return datetime.now(timezone.utc)
+
+
+def observation_time(day: date) -> datetime:
+ # Source calendar date, not an intraday timestamp.
+ return datetime.combine(day, time.min, tzinfo=timezone.utc)
+
+
+def numeric_value(raw: object, *, positive: bool = False) -> float:
+ try:
+ if isinstance(raw, bool) or raw is None:
+ raise ValueError
+ value = float(raw)
+ except (TypeError, ValueError, OverflowError):
+ raise ProviderError("Structured response contains a missing or invalid numeric value") from None
+ if not math.isfinite(value) or (positive and value <= 0):
+ raise ProviderError("Structured response contains an unsupported numeric value")
+ return value
+
+
+def observation_date(raw: object) -> date:
+ try:
+ if not isinstance(raw, str) or len(raw) != 10:
+ raise ValueError
+ parsed = date.fromisoformat(raw)
+ if parsed.isoformat() != raw:
+ raise ValueError
+ return parsed
+ except ValueError:
+ raise ProviderError("Structured response contains an invalid observation date") from None
+
+
+def select_window(rows: list[Observation], as_of: date, lookback_days: int) -> tuple[Observation, Observation, date]:
+ if not rows:
+ raise ProviderError("Structured response contains no observations")
+ if len({day for day, _ in rows}) != len(rows):
+ raise ProviderError("Structured response contains duplicate observation dates")
+ if any(day > as_of for day, _ in rows):
+ raise ProviderError("Structured response contains future observation dates")
+ ordered = sorted(rows)
+ latest = ordered[-1]
+ if (as_of - latest[0]).days > 14:
+ raise ProviderError("Latest structured observation is more than 14 days old")
+ target = latest[0] - timedelta(days=lookback_days)
+ baseline = [row for row in ordered if row[0] <= target]
+ if not baseline or (target - baseline[-1][0]).days > 7:
+ raise ProviderError("Structured response lacks a baseline within 7 days before the requested window")
+ return baseline[-1], latest, target
+
+
+async def fetch(url: str, params: dict[str, str], timeout: float, transport: httpx.AsyncBaseTransport | None) -> httpx.Response:
+ try:
+ async with httpx.AsyncClient(timeout=timeout, transport=transport, follow_redirects=False) as client:
+ response = await client.get(url, params=params)
+ response.raise_for_status()
+ return response
+ except httpx.HTTPStatusError as exc:
+ raise ProviderError(f"Structured provider returned HTTP {exc.response.status_code}") from None
+ except httpx.HTTPError:
+ raise ProviderError("Structured provider could not be reached") from None
+
+
+def fixture_rows(name: str) -> tuple[list[Observation], date]:
+ """Expand a declared synthetic step scenario; never impute live observations."""
+ payload = json.loads((Path(__file__).parent / "data" / f"{name}.json").read_text(encoding="utf-8"))
+ start = date.fromisoformat(payload["start_date"])
+ end = date.fromisoformat(payload["end_date"])
+ levels = [(date.fromisoformat(day), float(value)) for day, value in payload["levels"]]
+ rows = []
+ for offset in range((end - start).days + 1):
+ day = start + timedelta(days=offset)
+ rows.append((day, max((entry for entry in levels if entry[0] <= day), key=lambda entry: entry[0])[1]))
+ return rows, end
+
+
+def window_metadata(old: Observation, new: Observation, target: date, as_of: date, lookback_days: int) -> dict:
+ return {
+ "requested_lookback_days": lookback_days,
+ "requested_as_of": as_of.isoformat(),
+ "target_start_date": target.isoformat(),
+ "old_date": old[0].isoformat(),
+ "new_date": new[0].isoformat(),
+ "actual_window_days": (new[0] - old[0]).days,
+ "observation_lag_days": (as_of - new[0]).days,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ }
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/energy.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/energy.json
new file mode 100644
index 0000000..814bc6c
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/energy.json
@@ -0,0 +1,7 @@
+{
+ "synthetic": true,
+ "description": "Fixed daily step scenario for tests and judging. These are not EIA observations or retail fuel prices.",
+ "start_date": "2024-11-01",
+ "end_date": "2025-12-31",
+ "levels": [["2024-11-01", 80.0], ["2025-12-30", 92.0]]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/policy_rate.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/policy_rate.json
new file mode 100644
index 0000000..b39e5ef
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/data/policy_rate.json
@@ -0,0 +1,7 @@
+{
+ "synthetic": true,
+ "description": "Fixed daily step scenario for tests and judging. These are not ECB observations.",
+ "start_date": "2024-11-01",
+ "end_date": "2025-12-31",
+ "levels": [["2024-11-01", 3.0], ["2025-12-16", 3.25]]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/energy.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/energy.py
new file mode 100644
index 0000000..987b969
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/energy.py
@@ -0,0 +1,113 @@
+"""EIA API v2 Brent daily spot prices with explicit synthetic fallback."""
+from __future__ import annotations
+
+import os
+from collections.abc import Callable
+from datetime import date, datetime, timedelta
+
+import httpx
+
+from ..models import Confidence, EventType, MacroEvent, Provenance
+from .common import (
+ Observation, ProviderError, ProviderMode, fetch, fixture_rows, numeric_value,
+ observation_date, observation_time, select_window, utc_now, validate_request, window_metadata,
+)
+
+
+class EnergyProvider:
+ SERIES_ID = "PET.RBRTE.D"
+ API_URL = "https://api.eia.gov/v2/petroleum/pri/spt/data/"
+ SOURCE_URL = "https://www.eia.gov/dnav/pet/hist/RBRTED.htm"
+
+ def __init__(
+ self, api_key: str | None = None, timeout: float = 10.0, *,
+ transport: httpx.AsyncBaseTransport | None = None,
+ today: Callable[[], date] | None = None, now: Callable[[], datetime] = utc_now,
+ ):
+ self.api_key = api_key if api_key is not None else os.getenv("EIA_API_KEY", "")
+ self.timeout, self.transport, self.now = timeout, transport, now
+ self.today = today or (lambda: self.now().date())
+
+ @staticmethod
+ def _parse(payload: object) -> list[Observation]:
+ if not isinstance(payload, dict) or payload.get("error"):
+ raise ProviderError("EIA returned an invalid structured response")
+ response = payload.get("response")
+ if not isinstance(response, dict) or not isinstance(response.get("data"), list):
+ raise ProviderError("EIA response is missing required structured fields")
+ data = response["data"]
+ try:
+ if "total" in response and int(response["total"]) != len(data):
+ raise ProviderError("EIA response is incomplete; pagination or a narrower query is required")
+ except (TypeError, ValueError):
+ raise ProviderError("EIA response is incomplete or has an invalid record count") from None
+ rows = []
+ for row in data:
+ if not isinstance(row, dict) or row.get("series") != "RBRTE":
+ raise ProviderError("EIA response contains an unexpected series")
+ if row.get("units") != "Dollars per Barrel":
+ raise ProviderError("EIA response contains unexpected units")
+ rows.append((observation_date(row.get("period")), numeric_value(row.get("value"), positive=True)))
+ return rows
+
+ async def oil_move(self, lookback_days: int = 7, mode: ProviderMode = "live_or_fixture") -> MacroEvent:
+ validate_request(lookback_days, mode)
+ requested_as_of = self.today()
+ as_of, fallback_reason = requested_as_of, None
+ synthetic = mode == "fixture"
+ if not synthetic:
+ try:
+ if not self.api_key.strip():
+ raise ProviderError("EIA_API_KEY is not configured")
+ response = await fetch(self.API_URL, {
+ "api_key": self.api_key, "frequency": "daily", "data[0]": "value",
+ "facets[series][]": "RBRTE",
+ "start": (as_of - timedelta(days=lookback_days + 28)).isoformat(), "end": as_of.isoformat(),
+ "sort[0][column]": "period", "sort[0][direction]": "asc", "offset": "0", "length": "5000",
+ }, self.timeout, self.transport)
+ try:
+ rows = self._parse(response.json())
+ except (ValueError, TypeError) as exc:
+ if isinstance(exc, ProviderError):
+ raise
+ raise ProviderError("EIA response is not valid structured JSON") from None
+ old, new, target = select_window(rows, as_of, lookback_days)
+ numeric_value((new[1] / old[1] - 1) * 100)
+ except ProviderError as exc:
+ if mode == "live":
+ raise
+ synthetic, fallback_reason = True, str(exc)
+ if synthetic:
+ rows, as_of = fixture_rows("energy")
+ old, new, target = select_window(rows, as_of, lookback_days)
+ change_pct = (new[1] / old[1] - 1) * 100
+ metadata = window_metadata(old, new, target, as_of, lookback_days)
+ metadata.update({
+ "provider": "synthetic-fixture" if synthetic else "EIA",
+ "synthetic": synthetic, "mode": mode, "fallback_used": fallback_reason is not None,
+ "commodity": "Brent crude oil", "price_currency": "USD", "scope": "Brent Europe spot benchmark",
+ })
+ if synthetic:
+ metadata.update({"fixture_anchor_date": as_of.isoformat(), "live_request_as_of": requested_as_of.isoformat()})
+ if fallback_reason:
+ metadata["fallback_reason"] = fallback_reason
+ return MacroEvent(
+ event_id=f"{'synthetic' if synthetic else 'eia'}-brent-{old[0]}-{new[0]}", event_type=EventType.OIL_MOVE,
+ title=f"{'Synthetic scenario: ' if synthetic else ''}Brent crude moved {change_pct:+.2f}% over the observation window",
+ source="synthetic judging fixture" if synthetic else "U.S. Energy Information Administration",
+ observed_at=observation_time(new[0]), old_value=old[1], new_value=new[1], unit="USD per barrel", change_pct=change_pct,
+ evidence_url=None if synthetic else self.SOURCE_URL,
+ window_start=observation_time(old[0]), window_end=observation_time(new[0]),
+ confidence=Confidence.SCENARIO if synthetic else Confidence.OBSERVED,
+ provenance=Provenance(
+ provider="synthetic-fixture" if synthetic else "U.S. Energy Information Administration",
+ source_type="synthetic_fixture" if synthetic else "official_structured", retrieved_at=self.now(),
+ series_id="synthetic-brent-step-v1" if synthetic else self.SERIES_ID,
+ source_url=None if synthetic else self.API_URL,
+ notes=["Synthetic fixed step scenario; values and dates are not EIA observations."] if synthetic else [
+ "EIA API v2; frequency=daily, series=RBRTE, value in Dollars per Barrel.",
+ "Brent benchmark move only; household fuel pass-through is a separate explicit scenario assumption.",
+ "API key, keyed request URL, and remote request echoes are never retained in provenance.",
+ ],
+ ), metadata=metadata,
+ )
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fixtures.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fixtures.py
new file mode 100644
index 0000000..7148b99
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fixtures.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from datetime import datetime, timedelta, timezone
+
+from ..models import (
+ CommuteProfile,
+ Confidence,
+ EventType,
+ LifeProfile,
+ LifeUnit,
+ MacroEvent,
+ MortgageProfile,
+ Provenance,
+)
+
+
+def demo_events() -> list[MacroEvent]:
+ now = datetime(2026, 9, 10, 16, 0, tzinfo=timezone.utc)
+ return [
+ MacroEvent(
+ event_id="demo-jpy-sek-8pct",
+ event_type=EventType.FX_MOVE,
+ title="JPY strengthens sharply against SEK",
+ source="synthetic judging fixture",
+ observed_at=now,
+ old_value=15.00,
+ new_value=13.80,
+ unit="JPY per SEK",
+ change_pct=-8.0,
+ base_currency="SEK",
+ quote_currency="JPY",
+ affected_currencies=["SEK", "JPY"],
+ evidence_url=None,
+ window_start=now - timedelta(days=7),
+ window_end=now,
+ confidence=Confidence.SCENARIO,
+ provenance=Provenance(provider="demo-fixture", source_type="fixture", retrieved_at=now, notes=["Synthetic scenario; values and z-score are illustrative, not official observations."]),
+ metadata={"demo": True, "z_score": -3.2},
+ ),
+ MacroEvent(
+ event_id="demo-se-policy-rate-25bp",
+ event_type=EventType.INTEREST_RATE_CHANGE,
+ title="Swedish policy rate rises by 25 basis points",
+ source="demo-fixture",
+ observed_at=now,
+ old_value=4.00,
+ new_value=4.25,
+ unit="percent",
+ change_pct=6.25,
+ affected_currencies=["SEK"],
+ jurisdiction="SE",
+ confidence=Confidence.SCENARIO,
+ provenance=Provenance(provider="demo-fixture", source_type="fixture", retrieved_at=now, notes=["Synthetic scenario, not official observations."]),
+ metadata={"demo": True},
+ ),
+ MacroEvent(
+ event_id="demo-brent-15pct",
+ event_type=EventType.OIL_MOVE,
+ title="Brent crude rises 15% over the observation window",
+ source="demo-fixture",
+ observed_at=now,
+ old_value=80.0,
+ new_value=92.0,
+ unit="USD per barrel",
+ change_pct=15.0,
+ confidence=Confidence.SCENARIO,
+ provenance=Provenance(provider="demo-fixture", source_type="fixture", retrieved_at=now, notes=["Synthetic scenario; values and z-score are illustrative, not official observations."]),
+ metadata={"demo": True, "z_score": 2.4},
+ ),
+ ]
+
+
+def demo_profile() -> LifeProfile:
+ return LifeProfile(
+ home_currency="SEK",
+ home_country="SE",
+ net_monthly_income=32000,
+ monthly_work_hours=160,
+ travel_budget_home=20000,
+ travel_target_currency="JPY",
+ mortgage=MortgageProfile(
+ principal=2500000,
+ remaining_years=25,
+ current_annual_rate_pct=3.8,
+ currency="SEK",
+ ),
+ savings_balance=100000,
+ commute=CommuteProfile(monthly_fuel_liters=65, fuel_price_per_liter=18.5),
+ life_units=[
+ LifeUnit(name="beer", price=80, currency="SEK"),
+ LifeUnit(name="coffee", price=45, currency="SEK"),
+ LifeUnit(name="lunch", price=140, currency="SEK"),
+ ],
+ )
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/frankfurter.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/frankfurter.py
new file mode 100644
index 0000000..5c812cf
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/frankfurter.py
@@ -0,0 +1,182 @@
+from __future__ import annotations
+
+import math
+import re
+from collections.abc import Callable
+from datetime import date, datetime, time, timedelta, timezone
+from typing import Any
+
+import httpx
+
+from ..models import Confidence, EventType, MacroEvent, Provenance
+from .fx_anomaly import rolling_log_return_anomaly
+
+
+class FrankfurterProvider:
+ """Daily ECB reference rates, with an auditable comparison to prior volatility."""
+
+ def __init__(
+ self,
+ base_url: str = "https://api.frankfurter.dev",
+ timeout: float = 10.0,
+ *,
+ transport: httpx.AsyncBaseTransport | None = None,
+ now: Callable[[], datetime] | None = None,
+ ):
+ self.base_url = base_url.rstrip("/")
+ self.timeout = timeout
+ self.transport = transport
+ self.now = now or (lambda: datetime.now(timezone.utc))
+
+ @staticmethod
+ def _extract_rows(payload: Any) -> list[dict[str, Any]]:
+ # v2 returns an array. Retain compatible wrapper shapes without
+ # dropping malformed rows silently.
+ if isinstance(payload, dict):
+ if isinstance(payload.get("rates"), list):
+ payload = payload["rates"]
+ elif {"date", "rate"}.issubset(payload):
+ payload = [payload]
+ if not isinstance(payload, list) or any(not isinstance(row, dict) for row in payload):
+ raise ValueError("Unexpected Frankfurter response shape")
+ return payload
+
+ @staticmethod
+ def _validate_request(base: str, quote: str, lookback_days: int) -> tuple[str, str]:
+ currencies = []
+ for value in (base, quote):
+ if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z]{3}", value):
+ raise ValueError("FX currencies must be three ASCII letters")
+ currencies.append(value.upper())
+ if currencies[0] == currencies[1]:
+ raise ValueError("FX base and quote must be different currencies")
+ if type(lookback_days) is not int or not 2 <= lookback_days <= 90:
+ raise ValueError("lookback_days must be an integer from 2 to 90")
+ return currencies[0], currencies[1]
+
+ @staticmethod
+ def _observations(
+ rows: list[dict[str, Any]], base: str, quote: str, start: date, end: date
+ ) -> tuple[list[tuple[date, float]], int]:
+ observations: dict[date, float] = {}
+ duplicate_count = 0
+ for row in rows:
+ if str(row.get("base", "")).upper() != base or str(row.get("quote", "")).upper() != quote:
+ raise ValueError("Frankfurter returned a missing or mismatched currency pair")
+ raw_date = row.get("date")
+ try:
+ observed_date = date.fromisoformat(raw_date)
+ except (TypeError, ValueError) as exc:
+ raise ValueError("Frankfurter returned an invalid observation date") from exc
+ # Frankfurter can backfill a weekend/holiday `from` date to the
+ # previous reference observation. Permit only bounded start padding;
+ # future/end padding is never accepted.
+ if observed_date.isoformat() != raw_date or not start - timedelta(days=7) <= observed_date <= end:
+ raise ValueError("Frankfurter returned a non-ISO or out-of-query observation date")
+ raw_rate = row.get("rate")
+ if isinstance(raw_rate, bool) or not isinstance(raw_rate, (int, float, str)):
+ raise ValueError("Frankfurter returned an invalid FX rate")
+ try:
+ rate = float(raw_rate)
+ except (ValueError, OverflowError) as exc:
+ raise ValueError("Frankfurter returned an invalid FX rate") from exc
+ if not math.isfinite(rate) or rate <= 0:
+ raise ValueError("Frankfurter FX rates must be positive and finite")
+ if observed_date in observations:
+ if observations[observed_date] != rate:
+ raise ValueError("Frankfurter returned conflicting rates for one observation date")
+ duplicate_count += 1
+ observations[observed_date] = rate
+ return sorted(observations.items()), duplicate_count
+
+ async def fx_move(self, base: str, quote: str, lookback_days: int = 7) -> MacroEvent:
+ base, quote = self._validate_request(base, quote, lookback_days)
+ end = self.now().astimezone(timezone.utc).date()
+ # Two horizons plus a year and holiday buffer provide event endpoints
+ # and preceding comparison windows in one bounded call.
+ start = end - timedelta(days=2 * lookback_days + 380)
+ url = f"{self.base_url}/v2/providers/ecb/rates"
+ params = {"from": start.isoformat(), "to": end.isoformat(), "base": base, "quotes": quote}
+ async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
+ response = await client.get(url, params=params)
+ response.raise_for_status()
+ query_url = str(response.request.url)
+ rows = self._extract_rows(response.json())
+
+ observations, duplicate_count = self._observations(rows, base, quote, start, end)
+ if len(observations) < 2:
+ raise ValueError("Not enough FX observations returned")
+ new_date, new_rate = observations[-1]
+ if (end - new_date).days > 14:
+ raise ValueError("Latest FX observation is more than 14 calendar days old")
+ target_date = new_date - timedelta(days=lookback_days)
+ prior = [(day, rate) for day, rate in observations if day <= target_date]
+ if not prior:
+ raise ValueError("No FX observation on or before the requested lookback date")
+ old_date, old_rate = prior[-1]
+ if (target_date - old_date).days > 7:
+ raise ValueError("FX baseline observation is more than 7 calendar days before the requested anchor")
+ change_pct = (new_rate / old_rate - 1) * 100
+ if not math.isfinite(change_pct):
+ raise ValueError("FX percentage change is outside the supported numeric range")
+ observed = datetime.combine(new_date, time.min, timezone.utc)
+ window_start = datetime.combine(old_date, time.min, timezone.utc)
+ anomaly = rolling_log_return_anomaly(observations, old_date, new_date)
+ metadata = {
+ "old_date": old_date.isoformat(),
+ "new_date": new_date.isoformat(),
+ "provider": "ECB",
+ "requested_lookback_days": lookback_days,
+ "target_old_date": target_date.isoformat(),
+ "actual_calendar_days": (new_date - old_date).days,
+ "anchor_gap_days": (target_date - old_date).days,
+ "latest_observation_age_days": (end - new_date).days,
+ "query_start_date": start.isoformat(),
+ "query_end_date": end.isoformat(),
+ "history_start_date": observations[0][0].isoformat(),
+ "pre_query_observation_count": sum(day < start for day, _ in observations),
+ "query_url": query_url,
+ "history_observation_count": len(observations),
+ "identical_duplicates_removed": duplicate_count,
+ "anomaly": anomaly,
+ }
+ if anomaly["z_score"] is not None:
+ metadata["z_score"] = anomaly["z_score"]
+ notes = [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ f"Requested {lookback_days} calendar days; actual observed window is {old_date} through {new_date}.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials.",
+ ]
+ if anomaly["fallback_reason"]:
+ notes.append(f"Volatility score unavailable: {anomaly['fallback_reason']}; radar uses percentage-move fallback.")
+ if (end - new_date).days > 7:
+ notes.append(f"Latest reference observation is {(end - new_date).days} calendar days old.")
+ return MacroEvent(
+ event_id=f"fx-{base}-{quote}-{old_date}-{new_date}",
+ event_type=EventType.FX_MOVE,
+ title=f"{base}/{quote} moved {change_pct:+.2f}% from {old_date} to {new_date}",
+ source="Frankfurter / ECB",
+ observed_at=observed,
+ old_value=old_rate,
+ new_value=new_rate,
+ unit=f"{quote} per {base}",
+ change_pct=change_pct,
+ base_currency=base,
+ quote_currency=quote,
+ affected_currencies=[base, quote],
+ evidence_url="https://frankfurter.dev/providers/ecb/",
+ window_start=window_start,
+ window_end=observed,
+ confidence=Confidence.OBSERVED,
+ provenance=Provenance(
+ provider="Frankfurter (ECB reference-rate provider)",
+ source_type="structured_intermediary_official_provider",
+ retrieved_at=self.now().astimezone(timezone.utc),
+ series_id=f"ECB:{base}/{quote}",
+ source_url=query_url,
+ notes=notes,
+ ),
+ metadata=metadata,
+ )
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fx_anomaly.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fx_anomaly.py
new file mode 100644
index 0000000..ec823e8
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/fx_anomaly.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import math
+import statistics
+from datetime import date, timedelta
+from typing import Any
+
+
+MIN_REFERENCE_SAMPLES = 30
+MAX_REFERENCE_SAMPLES = 252
+VOLATILITY_EPSILON = 1e-12
+
+
+def rolling_log_return_anomaly(
+ observations: list[tuple[date, float]], old_date: date, new_date: date
+) -> dict[str, Any]:
+ """Compare an event against prior equal-calendar-horizon log returns.
+
+ Input observations must already be unique, positive, finite, and sorted.
+ Reference returns may overlap each other, but never the selected event.
+ Missing exact-horizon endpoints are omitted rather than interpolated.
+ """
+ rates = dict(observations)
+ horizon = (new_date - old_date).days
+ event_return = math.log(rates[new_date]) - math.log(rates[old_date])
+ references: list[tuple[date, date, float]] = []
+ for end_date, end_rate in observations:
+ if end_date > old_date:
+ break
+ start_date = end_date - timedelta(days=horizon)
+ if start_date in rates:
+ references.append((start_date, end_date, math.log(end_rate) - math.log(rates[start_date])))
+ references = references[-MAX_REFERENCE_SAMPLES:]
+ returns = [row[2] for row in references]
+ mean = statistics.fmean(returns) if returns else None
+ stdev = statistics.stdev(returns) if len(returns) >= 2 else None
+ result = {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": horizon,
+ "sample_count": len(returns),
+ "minimum_sample_count": MIN_REFERENCE_SAMPLES,
+ "maximum_sample_count": MAX_REFERENCE_SAMPLES,
+ "reference_window_start": references[0][0].isoformat() if references else None,
+ "reference_window_end": references[-1][1].isoformat() if references else None,
+ "reference_windows_may_overlap": True,
+ "event_excluded_from_reference": True,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": event_return,
+ "reference_mean_log_return": mean,
+ "reference_sample_stddev": stdev,
+ "z_score": None,
+ "fallback_reason": None,
+ }
+ if len(returns) < MIN_REFERENCE_SAMPLES:
+ result["fallback_reason"] = "insufficient_history"
+ elif stdev is None or stdev <= VOLATILITY_EPSILON:
+ result["fallback_reason"] = "zero_or_negligible_reference_volatility"
+ else:
+ z_score = (event_return - mean) / stdev
+ if math.isfinite(z_score):
+ result["z_score"] = z_score
+ else:
+ result["fallback_reason"] = "nonfinite_score"
+ return result
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/official_rss.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/official_rss.py
new file mode 100644
index 0000000..f10c8e1
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/official_rss.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+import hashlib
+from datetime import datetime
+from email.utils import parsedate_to_datetime
+from xml.etree import ElementTree as ET
+
+import httpx
+
+from ..models import EventType, MacroHeadline
+
+
+FEEDS = {
+ "fed": {
+ "publisher": "Federal Reserve Board",
+ "jurisdiction": "US",
+ "url": "https://www.federalreserve.gov/feeds/press_monetary.xml",
+ },
+ "ecb": {
+ "publisher": "European Central Bank",
+ "jurisdiction": "EU",
+ "url": "https://www.ecb.europa.eu/rss/press.html",
+ },
+}
+
+
+def _text(node: ET.Element | None) -> str | None:
+ return node.text.strip() if node is not None and node.text else None
+
+
+def _parse_date(value: str | None) -> datetime | None:
+ if not value:
+ return None
+ try:
+ return parsedate_to_datetime(value)
+ except (TypeError, ValueError):
+ try:
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
+ except ValueError:
+ return None
+
+
+def _classify(title: str) -> tuple[EventType | None, list[str]]:
+ lower = title.lower()
+ tags: list[str] = []
+ hint = None
+ if any(term in lower for term in ["monetary policy", "fomc", "interest rate", "policy rate", "rates"]):
+ hint = EventType.INTEREST_RATE_CHANGE
+ tags.append("monetary-policy")
+ if any(term in lower for term in ["inflation", "consumer prices", "price stability"]):
+ tags.append("inflation")
+ if any(term in lower for term in ["economic outlook", "projections", "forecast"]):
+ tags.append("outlook")
+ return hint, tags
+
+
+def parse_feed(xml_text: str, source: str, limit: int = 20) -> list[MacroHeadline]:
+ if source not in FEEDS:
+ raise ValueError("Unsupported official RSS source")
+ if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 50:
+ raise ValueError("limit must be an integer from 1 to 50")
+ cfg = FEEDS[source]
+ root = ET.fromstring(xml_text)
+ items = root.findall(".//item")
+ headlines: list[MacroHeadline] = []
+ for item in items[:limit]:
+ title = _text(item.find("title"))
+ link = _text(item.find("link"))
+ published = _text(item.find("pubDate")) or _text(item.find("date"))
+ if not title or not link:
+ continue
+ hint, tags = _classify(title)
+ digest = hashlib.sha256(f"{source}|{link}".encode()).hexdigest()[:16]
+ headlines.append(
+ MacroHeadline(
+ headline_id=f"{source}-{digest}",
+ title=title,
+ publisher=cfg["publisher"],
+ published_at=_parse_date(published),
+ url=link,
+ jurisdiction=cfg["jurisdiction"],
+ event_type_hint=hint,
+ tags=tags,
+ )
+ )
+ return headlines
+
+
+class OfficialRSSProvider:
+ def __init__(self, timeout: float = 10.0):
+ self.timeout = timeout
+
+ async def headlines(self, source: str, limit: int = 20) -> list[MacroHeadline]:
+ source = source.lower()
+ if source not in FEEDS:
+ raise ValueError(f"Unsupported official RSS source: {source}")
+ if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 50:
+ raise ValueError("limit must be an integer from 1 to 50")
+ async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client:
+ response = await client.get(FEEDS[source]["url"])
+ response.raise_for_status()
+ return parse_feed(response.text, source=source, limit=limit)
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/policy_rate.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/policy_rate.py
new file mode 100644
index 0000000..8efc119
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/providers/policy_rate.py
@@ -0,0 +1,104 @@
+"""Official ECB deposit-facility daily levels; headlines never supply values."""
+from __future__ import annotations
+
+import csv
+import io
+from collections.abc import Callable
+from datetime import date, datetime, timedelta
+
+import httpx
+
+from ..models import Confidence, EventType, MacroEvent, Provenance
+from .common import (
+ Observation, ProviderError, ProviderMode, fetch, fixture_rows, numeric_value,
+ observation_date, observation_time, select_window, utc_now, validate_request, window_metadata,
+)
+
+
+class PolicyRateProvider:
+ SERIES_ID = "FM.D.U2.EUR.4F.KR.DFR.LEV"
+ SOURCE_URL = "https://data.ecb.europa.eu/data/datasets/FM/FM.D.U2.EUR.4F.KR.DFR.LEV"
+ API_URL = "https://data-api.ecb.europa.eu/service/data/FM/D.U2.EUR.4F.KR.DFR.LEV"
+
+ def __init__(
+ self, timeout: float = 10.0, *, transport: httpx.AsyncBaseTransport | None = None,
+ today: Callable[[], date] | None = None, now: Callable[[], datetime] = utc_now,
+ ):
+ self.timeout, self.transport, self.now = timeout, transport, now
+ self.today = today or (lambda: self.now().date())
+
+ @classmethod
+ def _parse(cls, content: str) -> list[Observation]:
+ reader = csv.DictReader(io.StringIO(content.lstrip("\ufeff")))
+ if not {"KEY", "TIME_PERIOD", "OBS_VALUE", "UNIT", "UNIT_MULT"}.issubset(reader.fieldnames or []):
+ raise ProviderError("ECB response is missing required structured fields")
+ if len(reader.fieldnames) != len(set(reader.fieldnames)):
+ raise ProviderError("ECB response contains duplicate structured fields")
+ rows = []
+ try:
+ for row in reader:
+ if row.get("KEY") != cls.SERIES_ID:
+ raise ProviderError("ECB response contains an unexpected series")
+ if row.get("UNIT") not in ("PC", "PCPA") or row.get("UNIT_MULT") != "0":
+ raise ProviderError("ECB response contains unexpected units")
+ if row.get("OBS_STATUS") in {"M", "L"}:
+ raise ProviderError("ECB response contains an unavailable observation")
+ rows.append((observation_date(row.get("TIME_PERIOD")), numeric_value(row.get("OBS_VALUE"))))
+ except csv.Error:
+ raise ProviderError("ECB response is not valid structured CSV") from None
+ return rows
+
+ async def policy_rate_move(self, lookback_days: int = 30, mode: ProviderMode = "live_or_fixture") -> MacroEvent:
+ validate_request(lookback_days, mode)
+ requested_as_of = self.today()
+ as_of, fallback_reason = requested_as_of, None
+ synthetic = mode == "fixture"
+ if not synthetic:
+ try:
+ response = await fetch(self.API_URL, {
+ "startPeriod": (as_of - timedelta(days=lookback_days + 28)).isoformat(),
+ "endPeriod": as_of.isoformat(), "format": "csvdata",
+ }, self.timeout, self.transport)
+ rows = self._parse(response.text)
+ old, new, target = select_window(rows, as_of, lookback_days)
+ numeric_value((new[1] - old[1]) * 100)
+ except ProviderError as exc:
+ if mode == "live":
+ raise
+ synthetic, fallback_reason = True, str(exc)
+ if synthetic:
+ rows, as_of = fixture_rows("policy_rate")
+ old, new, target = select_window(rows, as_of, lookback_days)
+ delta_bp = (new[1] - old[1]) * 100
+ metadata = window_metadata(old, new, target, as_of, lookback_days)
+ metadata.update({
+ "provider": "synthetic-fixture" if synthetic else "ECB",
+ "synthetic": synthetic, "mode": mode, "fallback_used": fallback_reason is not None,
+ "change_basis_points": delta_bp, "change_percentage_points": new[1] - old[1],
+ "scope": "Euro area; EUR exposures only", "rate_name": "deposit facility",
+ })
+ if synthetic:
+ metadata.update({"fixture_anchor_date": as_of.isoformat(), "live_request_as_of": requested_as_of.isoformat()})
+ if fallback_reason:
+ metadata["fallback_reason"] = fallback_reason
+ return MacroEvent(
+ event_id=f"{'synthetic' if synthetic else 'ecb'}-deposit-rate-{old[0]}-{new[0]}",
+ event_type=EventType.INTEREST_RATE_CHANGE,
+ title=f"{'Synthetic scenario: ' if synthetic else ''}Euro-area deposit facility rate moved {delta_bp:+.1f} basis points",
+ source="synthetic judging fixture" if synthetic else "European Central Bank",
+ observed_at=observation_time(new[0]), old_value=old[1], new_value=new[1],
+ unit="percent", change_pct=None, affected_currencies=["EUR"], jurisdiction="EA",
+ evidence_url=None if synthetic else self.SOURCE_URL,
+ window_start=observation_time(old[0]), window_end=observation_time(new[0]),
+ confidence=Confidence.SCENARIO if synthetic else Confidence.OBSERVED,
+ provenance=Provenance(
+ provider="synthetic-fixture" if synthetic else "European Central Bank",
+ source_type="synthetic_fixture" if synthetic else "official_structured",
+ retrieved_at=self.now(), series_id="synthetic-policy-rate-step-v1" if synthetic else self.SERIES_ID,
+ source_url=None if synthetic else self.API_URL,
+ notes=["Synthetic fixed step scenario; values and dates are not ECB observations."] if synthetic else [
+ "ECB daily deposit facility levels, percent per annum; signed basis-point change is 100 × (new − old).",
+ "Daily levels measure the requested window; this does not identify a specific monetary-policy announcement.",
+ ],
+ ), metadata=metadata,
+ )
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/radar.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/radar.py
new file mode 100644
index 0000000..f5844e5
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/radar.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import math
+
+from .models import EventScore, EventType, LifeProfile, MacroEvent
+
+
+def _clamp(value: float) -> float:
+ return max(0.0, min(100.0, round(value, 1)))
+
+
+def score_market_significance(event: MacroEvent) -> tuple[float, str]:
+ """Transparent heuristic. If a provider supplies a z-score, prefer it over type-specific fallbacks."""
+ z = event.metadata.get("z_score")
+ if isinstance(z, (int, float)) and not isinstance(z, bool):
+ try:
+ numeric_z = float(z)
+ except OverflowError:
+ numeric_z = None
+ if numeric_z is not None and math.isfinite(numeric_z):
+ return _clamp(abs(numeric_z) * 25.0), "provider_z_score"
+
+ if event.event_type == EventType.FX_MOVE:
+ move = abs(event.change_pct or 0.0)
+ return _clamp(move * 18.0), "fallback_fx_percent_move"
+ if event.event_type == EventType.OIL_MOVE:
+ move = abs(event.change_pct or 0.0)
+ return _clamp(move * 6.0), "fallback_oil_percent_move"
+ if event.event_type == EventType.INTEREST_RATE_CHANGE:
+ basis_points = abs(event.new_value - event.old_value) * 100
+ return _clamp(basis_points * 1.8), "fallback_policy_rate_basis_points"
+ return 0.0, "unsupported"
+
+
+def score_user_relevance(event: MacroEvent, profile: LifeProfile) -> tuple[float, list[str]]:
+ score = 0.0
+ reasons: list[str] = []
+
+ if event.event_type == EventType.FX_MOVE:
+ pair = {event.base_currency, event.quote_currency}
+ if profile.home_currency not in pair or (profile.travel_target_currency and profile.travel_target_currency not in pair):
+ return 0.0, ["FX pair does not match the declared home/travel currency exposure"]
+ if profile.home_currency in pair:
+ score += 35
+ reasons.append("FX pair includes the user's home currency")
+ if profile.travel_target_currency and profile.travel_target_currency in pair:
+ score += 45
+ reasons.append("FX pair includes the user's travel target currency")
+ if profile.travel_budget_home is not None:
+ score += 20
+ reasons.append("User supplied a travel budget")
+
+ elif event.event_type == EventType.INTEREST_RATE_CHANGE:
+ exposure_currency = profile.home_currency
+ mortgage_eligible = bool(profile.mortgage and profile.mortgage_currency == profile.home_currency)
+ if exposure_currency not in event.affected_currencies:
+ return 0.0, ["Policy-rate currency scope does not support a direct home-currency impact"]
+ if not mortgage_eligible and not profile.savings_balance:
+ return 0.0, ["No eligible home-currency mortgage or savings exposure"]
+ if mortgage_eligible:
+ score += 40
+ reasons.append("User has declared mortgage exposure")
+ if profile.savings_balance:
+ score += 20
+ reasons.append("User has declared savings exposure")
+ if not event.affected_currencies or exposure_currency in event.affected_currencies:
+ score += 30
+ reasons.append("Policy-rate scope matches the user's declared currency exposure")
+ if profile.home_country and event.jurisdiction and profile.home_country == event.jurisdiction:
+ score += 10
+ reasons.append("Policy jurisdiction matches the user's home country")
+
+ elif event.event_type == EventType.OIL_MOVE:
+ if not profile.commute or profile.commute.monthly_fuel_liters == 0:
+ return 0.0, ["No positive monthly fuel consumption declared"]
+ if profile.commute:
+ score += 80
+ reasons.append("User supplied monthly fuel use and pump price")
+ if profile.commute and profile.commute.monthly_fuel_liters > 0:
+ score += 20
+ reasons.append("User has direct driving exposure")
+
+ return _clamp(score), reasons
+
+
+def rank_events(events: list[MacroEvent], profile: LifeProfile) -> list[EventScore]:
+ scored: list[EventScore] = []
+ for event in events:
+ significance, method = score_market_significance(event)
+ relevance, reasons = score_user_relevance(event, profile)
+ # Relevance is weighted slightly more heavily because the product thesis is personal impact, not market news ranking.
+ priority = _clamp(significance * 0.45 + relevance * 0.55)
+ scored.append(
+ EventScore(
+ event=event,
+ market_significance=significance,
+ user_relevance=relevance,
+ priority_score=priority,
+ significance_method=method,
+ relevance_reasons=reasons,
+ )
+ )
+ return sorted(scored, key=lambda item: item.priority_score, reverse=True)
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/service.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/service.py
new file mode 100644
index 0000000..261a83f
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/source/life_exchange_rate/service.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import os
+
+from .calculations import convert_amount_to_life_units, translate_event
+from .models import Confidence, ConvertRequest, MacroEvent, RadarRequest, ScenarioRequest, TranslateRequest
+from .providers.fixtures import demo_events, demo_profile
+from .providers.frankfurter import FrankfurterProvider
+from .providers.official_rss import OfficialRSSProvider
+from .providers.policy_rate import PolicyRateProvider
+from .providers.energy import EnergyProvider
+from .radar import rank_events
+
+
+def get_demo_events() -> list[MacroEvent]:
+ return demo_events()
+
+
+def get_demo_radar() -> list[dict]:
+ return [item.model_dump(mode="json") for item in rank_events(demo_events(), demo_profile())]
+
+
+async def get_live_fx_event(base: str, quote: str, lookback_days: int = 7) -> MacroEvent:
+ provider = FrankfurterProvider(base_url=os.getenv("FRANKFURTER_BASE_URL", "https://api.frankfurter.dev"))
+ return await provider.fx_move(base=base, quote=quote, lookback_days=lookback_days)
+
+
+async def get_policy_rate_event(lookback_days: int = 30, mode: str = "live_or_fixture") -> MacroEvent:
+ return await PolicyRateProvider().policy_rate_move(lookback_days=lookback_days, mode=mode)
+
+
+async def get_energy_event(lookback_days: int = 7, mode: str = "live_or_fixture") -> MacroEvent:
+ return await EnergyProvider().oil_move(lookback_days=lookback_days, mode=mode)
+
+
+async def get_official_headlines(source: str, limit: int = 10) -> list[dict]:
+ provider = OfficialRSSProvider()
+ items = await provider.headlines(source=source, limit=limit)
+ return [item.model_dump(mode="json") for item in items]
+
+
+def rank_request(request: RadarRequest) -> list[dict]:
+ return [item.model_dump(mode="json") for item in rank_events(request.events, request.profile)]
+
+
+def convert_request(request: ConvertRequest) -> dict:
+ if request.currency != request.profile.home_currency:
+ raise ValueError("MVP conversion requires amount currency to match profile.home_currency")
+ work_hours, units = convert_amount_to_life_units(abs(request.amount), request.profile)
+ return {
+ "amount": request.amount,
+ "currency": request.currency,
+ "work_hours_equivalent": work_hours,
+ "life_units": [item.model_dump() for item in units],
+ }
+
+
+def translate_request(request: TranslateRequest) -> dict:
+ return translate_event(request).model_dump(mode="json")
+
+
+def compare_scenarios(request: ScenarioRequest) -> list[dict]:
+ results: list[dict] = []
+ for scenario in request.scenarios:
+ event = request.event.model_copy(deep=True)
+ event.new_value = request.event.old_value + (request.event.new_value - request.event.old_value) * scenario.multiplier
+ event.change_pct = None
+ event.confidence = Confidence.SCENARIO
+ event.metadata.pop("z_score", None)
+ event.metadata.pop("anomaly", None)
+ event.metadata["scenario_multiplier"] = scenario.multiplier
+ event = MacroEvent.model_validate(event.model_dump())
+ translated = translate_event(
+ TranslateRequest(event=event, profile=request.profile, assumptions=request.assumptions)
+ )
+ results.append({"label": scenario.label, "result": translated.model_dump(mode="json")})
+ return results
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_api.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_api.py
new file mode 100644
index 0000000..feff733
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_api.py
@@ -0,0 +1,126 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from life_exchange_rate import __version__
+from life_exchange_rate import main
+from life_exchange_rate.providers.fixtures import demo_profile
+
+
+@pytest.fixture(scope="module")
+def client():
+ # MCP's session manager is single-use: one lifespan for this app instance.
+ with TestClient(main.app, base_url="http://localhost:8000") as test_client:
+ yield test_client
+
+
+def test_health_and_verification(client, monkeypatch):
+ monkeypatch.setenv("REVIEW_COMMIT", "review-test-commit")
+ monkeypatch.setenv("APP_VERSION", "ignored-stale-version")
+ health = client.get("/health")
+ assert health.status_code == 200
+ assert health.json()["status"] == "ok"
+ assert health.json()["mcp"] == "enabled"
+ proof = client.get("/.well-known/xagent-verification.json")
+ assert proof.status_code == 200
+ assert proof.json()["schemaVersion"] == 1
+ assert health.json()["commit"] == proof.json()["commit"] == "review-test-commit"
+ assert health.json()["version"] == proof.json()["version"] == __version__
+ assert proof.json()["mcp"].endswith("/mcp/")
+
+
+def test_demo_events_and_radar_are_available(client):
+ events = client.get("/v1/events/demo")
+ assert events.status_code == 200
+ assert len(events.json()) == 3
+ radar = client.get("/v1/radar/demo")
+ assert radar.status_code == 200
+ assert len(radar.json()) == 3
+ assert radar.json()[0]["priority_score"] >= radar.json()[-1]["priority_score"]
+
+
+@pytest.mark.parametrize("path", ["policy-rate", "energy"])
+def test_structured_providers_have_reproducible_fixture_mode(client, path):
+ response = client.get(f"/v1/events/{path}", params={"mode": "fixture"})
+ assert response.status_code == 200
+ event = response.json()
+ assert event["provenance"]["source_type"] == "synthetic_fixture"
+ assert event["provenance"]["source_url"] is None
+ assert event["metadata"]["synthetic"] is True
+ assert event["confidence"] == "scenario"
+ assert event["window_start"] <= event["window_end"]
+
+
+@pytest.mark.parametrize("path,params", [
+ ("/v1/events/fx", {"base": "SEK", "quote": "SEK"}),
+ ("/v1/events/fx", {"base": "12!", "quote": "JPY"}),
+ ("/v1/events/fx", {"base": "SEK", "quote": "JPY", "lookback_days": 1}),
+ ("/v1/events/policy-rate", {"mode": "invent"}),
+ ("/v1/events/energy", {"lookback_days": 366}),
+ ("/v1/headlines/official", {"source": "untrusted"}),
+ ("/v1/headlines/official", {"source": "fed", "limit": 0}),
+])
+def test_provider_inputs_are_validated_before_fetch(client, path, params):
+ assert client.get(path, params=params).status_code == 422
+
+
+@pytest.mark.parametrize("path,provider,params", [
+ ("/v1/events/fx", "get_live_fx_event", {"base": "SEK", "quote": "JPY"}),
+ ("/v1/events/policy-rate", "get_policy_rate_event", {"mode": "live"}),
+ ("/v1/events/energy", "get_energy_event", {"mode": "live"}),
+ ("/v1/headlines/official", "get_official_headlines", {"source": "fed"}),
+])
+def test_upstream_errors_do_not_expose_sensitive_urls(client, monkeypatch, path, provider, params):
+ async def failing_provider(*args, **kwargs):
+ raise RuntimeError("request failed: https://upstream.invalid/?api_key=DO_NOT_LEAK")
+
+ monkeypatch.setattr(main, provider, failing_provider)
+ response = client.get(path, params=params)
+ assert response.status_code == 502
+ assert "DO_NOT_LEAK" not in response.text
+ assert "upstream.invalid" not in response.text
+
+
+def test_headline_cannot_be_used_as_quantified_event(client):
+ response = client.post("/v1/translate", json={
+ "event": {"headline_id": "headline-only", "title": "An official policy announcement", "requires_quantification": True},
+ "profile": demo_profile().model_dump(mode="json"),
+ })
+ assert response.status_code == 422
+
+
+def test_public_url_builds_narrow_mcp_allowlist(monkeypatch):
+ monkeypatch.setenv("PUBLIC_BASE_URL", "https://review.example:9443/service")
+ settings = main._transport_security()
+ assert settings.enable_dns_rebinding_protection
+ assert "review.example:9443" in settings.allowed_hosts
+ assert "review.example" in settings.allowed_hosts
+ assert "https://review.example:9443" in settings.allowed_origins
+ assert "*" not in settings.allowed_hosts
+ assert "review.example:*" not in settings.allowed_hosts
+
+
+def test_mcp_mount_accepts_localhost_and_rejects_other_hosts_and_origins(client):
+ request = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
+ "protocolVersion": "2025-11-25", "capabilities": {},
+ "clientInfo": {"name": "rest-mount-test", "version": "1"},
+ }}
+ headers = {"Accept": "application/json, text/event-stream"}
+ response = client.post("/mcp/", json=request, headers=headers)
+ assert response.status_code == 200
+ assert response.json()["result"]["protocolVersion"] == "2025-11-25"
+ denied_host = client.post("/mcp/", json=request, headers={**headers, "Host": "untrusted.example"})
+ assert denied_host.status_code == 421
+ denied_origin = client.post("/mcp/", json=request, headers={**headers, "Origin": "https://untrusted.example"})
+ assert denied_origin.status_code == 403
+
+
+@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")])
+def test_nonfinite_json_input_returns_safe_validation_error(client, value):
+ import json
+ from life_exchange_rate.providers.fixtures import demo_events
+ event = demo_events()[0].model_dump(mode="json")
+ event["new_value"] = value
+ body = json.dumps({"event": event, "profile": demo_profile().model_dump(mode="json")})
+ response = client.post("/v1/translate", content=body, headers={"Content-Type": "application/json"})
+ assert response.status_code == 422
+ assert response.json()["detail"]
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_calculations.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_calculations.py
new file mode 100644
index 0000000..43e1b12
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_calculations.py
@@ -0,0 +1,98 @@
+from life_exchange_rate.models import (
+ CommuteProfile,
+ EventType,
+ LifeProfile,
+ LifeUnit,
+ MacroEvent,
+ MortgageProfile,
+ TranslateRequest,
+)
+from life_exchange_rate.service import translate_request
+
+
+def profile() -> LifeProfile:
+ return LifeProfile(
+ home_currency="SEK",
+ home_country="SE",
+ net_monthly_income=32000,
+ monthly_work_hours=160,
+ travel_budget_home=20000,
+ travel_target_currency="JPY",
+ mortgage=MortgageProfile(principal=2500000, remaining_years=25, current_annual_rate_pct=3.8, currency="SEK"),
+ savings_balance=100000,
+ commute=CommuteProfile(monthly_fuel_liters=65, fuel_price_per_liter=18.5),
+ life_units=[LifeUnit(name="beer", price=80, currency="SEK")],
+ )
+
+
+def test_fx_stronger_jpy_requires_more_home_budget_and_trace():
+ event = MacroEvent(
+ event_id="fx",
+ event_type=EventType.FX_MOVE,
+ title="JPY strengthens",
+ source="test",
+ old_value=15.0,
+ new_value=13.8,
+ unit="JPY per SEK",
+ base_currency="SEK",
+ quote_currency="JPY",
+ )
+ result = translate_request(TranslateRequest(event=event, profile=profile()))
+ assert result["direct_effect_home"] > 0
+ assert 1700 < result["direct_effect_home"] < 1800
+ assert result["work_hours_equivalent"] > 8
+ assert result["direction"] == "cost_increase"
+ assert len(result["calculation_trace"]) >= 4
+
+
+def test_interest_rate_result_is_scenario_labeled():
+ event = MacroEvent(
+ event_id="rate",
+ event_type=EventType.INTEREST_RATE_CHANGE,
+ title="Swedish rate up",
+ source="test",
+ old_value=4.0,
+ new_value=4.25,
+ unit="percent",
+ affected_currencies=["SEK"],
+ jurisdiction="SE",
+ )
+ result = translate_request(TranslateRequest(event=event, profile=profile()))
+ assert result["confidence"] == "scenario"
+ assert result["assumptions"]
+ assert result["details"]["mortgage_monthly_delta"] > 0
+
+
+def test_foreign_policy_rate_does_not_apply_directly_to_sek_mortgage():
+ event = MacroEvent(
+ event_id="fed",
+ event_type=EventType.INTEREST_RATE_CHANGE,
+ title="Fed rate up",
+ source="test",
+ old_value=5.0,
+ new_value=5.25,
+ unit="percent",
+ affected_currencies=["USD"],
+ jurisdiction="US",
+ )
+ result = translate_request(TranslateRequest(event=event, profile=profile()))
+ assert result["direct_effect_home"] is None
+ assert result["direction"] == "unquantified"
+ assert "outside the MVP" in result["warnings"][0]
+
+
+def test_oil_result_warns_about_pass_through():
+ event = MacroEvent(
+ event_id="oil",
+ event_type=EventType.OIL_MOVE,
+ title="Brent up",
+ source="test",
+ old_value=80,
+ new_value=92,
+ unit="USD per barrel",
+ change_pct=15,
+ )
+ result = translate_request(TranslateRequest(event=event, profile=profile()))
+ assert result["direct_effect_home"] > 0
+ assert any("one-for-one" in warning for warning in result["warnings"])
+ assert result["calculation_trace"]
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_deployment_config.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_deployment_config.py
new file mode 100644
index 0000000..243a349
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_deployment_config.py
@@ -0,0 +1,25 @@
+from life_exchange_rate import main
+
+
+def test_vercel_revision_is_platform_bound(monkeypatch):
+ monkeypatch.setenv("VERCEL_GIT_COMMIT_SHA", "a" *40)
+ monkeypatch.setenv("REVIEW_COMMIT", "stale-local")
+ assert main.health()["commit"] == "a" *40
+ assert main.xagent_verification()["commit"] == "a" *40
+
+
+def test_vercel_origin_and_submission_binding(monkeypatch):
+ monkeypatch.delenv("PUBLIC_BASE_URL", raising=False)
+ monkeypatch.setenv("VERCEL_PROJECT_PRODUCTION_URL", "project.example")
+ monkeypatch.setenv("SUBMISSION_SLUG", "kongkoukk-life-exchange-rate")
+ proof = main.xagent_verification()
+ assert proof["slug"] == "kongkoukk-life-exchange-rate"
+ assert proof["apiBaseUrl"] == "https://project.example"
+ assert proof["mcp"] == "https://project.example/mcp/"
+ assert "project.example" in main._transport_security().allowed_hosts
+
+
+def test_self_hosted_origin_override(monkeypatch):
+ monkeypatch.setenv("VERCEL_PROJECT_PRODUCTION_URL", "project.example")
+ monkeypatch.setenv("PUBLIC_BASE_URL", "https://custom.example/")
+ assert main._base_url() == "https://custom.example"
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_frankfurter.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_frankfurter.py
new file mode 100644
index 0000000..340a765
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_frankfurter.py
@@ -0,0 +1,291 @@
+import math
+from datetime import date, datetime, timedelta, timezone
+
+import httpx
+import pytest
+
+from life_exchange_rate.providers.frankfurter import FrankfurterProvider
+from life_exchange_rate.radar import score_market_significance
+
+
+NOW = datetime(2026, 9, 14, 12, 0, tzinfo=timezone.utc)
+
+
+def row(day, rate=15.0, **changes):
+ return {"date": day.isoformat() if isinstance(day, date) else day, "base": "SEK", "quote": "JPY", "rate": rate, **changes}
+
+
+def history(days=350, shock=0.92):
+ start = NOW.date() - timedelta(days=days)
+ result = []
+ for index in range(days + 1):
+ day = start + timedelta(days=index)
+ if day.weekday() < 5 and day <= date(2026, 9, 11):
+ result.append(row(day, 15 * math.exp(0.005 * math.sin(index / 4) + 0.004 * math.cos(index / 11))))
+ result[-1]["rate"] *= shock
+ return result
+
+
+def provider_for(payload, requests=None, status_code=200):
+ def handler(request):
+ if requests is not None:
+ requests.append(request)
+ return httpx.Response(status_code, json=payload)
+
+ return FrankfurterProvider(transport=httpx.MockTransport(handler), now=lambda: NOW)
+
+
+@pytest.mark.asyncio
+async def test_live_move_selects_latest_minus_horizon_and_keeps_query_provenance():
+ payload = history()
+ requests = []
+ event = await provider_for(list(reversed(payload)), requests).fx_move("sek", "jpy", 7)
+ assert event.window_end.date() == date(2026, 9, 11)
+ assert event.window_start.date() == date(2026, 9, 4)
+ assert event.metadata["requested_lookback_days"] == 7
+ assert event.metadata["actual_calendar_days"] == 7
+ rates = {item["date"]: item["rate"] for item in payload}
+ assert event.old_value == rates["2026-09-04"]
+ assert event.new_value == rates["2026-09-11"]
+ assert event.change_pct == pytest.approx((rates["2026-09-11"] / rates["2026-09-04"] - 1) * 100)
+ assert requests[0].url.path == "/v2/providers/ecb/rates"
+ assert requests[0].url.params["base"] == "SEK"
+ assert requests[0].url.params["quotes"] == "JPY"
+ assert requests[0].url.params["from"] == (NOW.date() - timedelta(days=394)).isoformat()
+ assert requests[0].url.params["to"] == NOW.date().isoformat()
+ assert event.provenance.source_url == str(requests[0].url)
+ assert event.provenance.retrieved_at == NOW
+ assert event.observed_at.tzinfo == timezone.utc
+ assert "2026-09-04-2026-09-11" in event.event_id
+
+
+@pytest.mark.asyncio
+async def test_weekend_anchor_uses_equal_actual_calendar_horizon():
+ event = await provider_for(history()).fx_move("SEK", "JPY", 5)
+ assert event.metadata["target_old_date"] == "2026-09-06"
+ assert event.metadata["old_date"] == "2026-09-04"
+ assert event.metadata["anchor_gap_days"] == 2
+ assert event.metadata["anomaly"]["horizon_calendar_days"] == 7
+
+
+@pytest.mark.asyncio
+async def test_z_score_matches_prior_equal_horizon_returns_and_excludes_event():
+ payload = history()
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ rates = {date.fromisoformat(item["date"]): item["rate"] for item in payload}
+ old = date(2026, 9, 4)
+ references = [
+ math.log(rates[day] / rates[day - timedelta(days=7)])
+ for day in sorted(rates)
+ if day <= old and day - timedelta(days=7) in rates
+ ][-252:]
+ mean = sum(references) / len(references)
+ stddev = math.sqrt(sum((value - mean) ** 2 for value in references) / (len(references) - 1))
+ expected = (math.log(event.new_value / event.old_value) - mean) / stddev
+ anomaly = event.metadata["anomaly"]
+ assert anomaly["sample_count"] == len(references)
+ assert anomaly["reference_mean_log_return"] == pytest.approx(mean, abs=1e-14)
+ assert anomaly["reference_sample_stddev"] == pytest.approx(stddev)
+ assert event.metadata["z_score"] == pytest.approx(expected)
+ assert anomaly["reference_window_end"] <= event.metadata["old_date"]
+ assert anomaly["fallback_reason"] is None
+ assert score_market_significance(event)[1] == "provider_z_score"
+ changed = [dict(item) for item in payload]
+ for item in changed:
+ if item["date"] > old.isoformat():
+ item["rate"] *= 1.1
+ changed_event = await provider_for(changed).fx_move("SEK", "JPY")
+ changed_anomaly = changed_event.metadata["anomaly"]
+ assert changed_anomaly["reference_mean_log_return"] == anomaly["reference_mean_log_return"]
+ assert changed_anomaly["reference_sample_stddev"] == anomaly["reference_sample_stddev"]
+ assert changed_event.metadata["z_score"] != event.metadata["z_score"]
+
+
+@pytest.mark.asyncio
+async def test_insufficient_history_is_explicit_and_does_not_invent_score():
+ event = await provider_for(history(days=20)).fx_move("SEK", "JPY")
+ assert event.metadata["anomaly"]["fallback_reason"] == "insufficient_history"
+ assert event.metadata["anomaly"]["sample_count"] < 30
+ assert event.metadata["anomaly"]["z_score"] is None
+ assert "z_score" not in event.metadata
+ assert score_market_significance(event)[1] == "fallback_fx_percent_move"
+
+
+@pytest.mark.asyncio
+async def test_zero_volatility_falls_back_even_when_event_is_large():
+ payload = history()
+ for item in payload:
+ item["rate"] = 15.0
+ payload[-1]["rate"] = 13.8
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["anomaly"]["fallback_reason"] == "zero_or_negligible_reference_volatility"
+ assert "z_score" not in event.metadata
+ assert event.change_pct == pytest.approx(-8)
+
+
+@pytest.mark.asyncio
+async def test_identical_duplicates_do_not_inflate_reference_sample_count():
+ payload = history()
+ baseline = await provider_for(payload).fx_move("SEK", "JPY")
+ repeated = await provider_for(payload + payload[:20]).fx_move("SEK", "JPY")
+ assert repeated.metadata["identical_duplicates_removed"] == 20
+ assert repeated.metadata["anomaly"] == baseline.metadata["anomaly"]
+
+
+@pytest.mark.asyncio
+async def test_missing_target_anchor_fails_without_shortening_horizon():
+ with pytest.raises(ValueError, match="on or before"):
+ await provider_for([row("2026-09-10"), row("2026-09-11")]).fx_move("SEK", "JPY")
+
+
+@pytest.mark.parametrize("bad_row", [
+ row("2026-09-10", quote="USD"),
+ row("2026-09-10", base="USD"),
+ {"date": "2026-09-10", "rate": 15},
+ row("2026-09-32"),
+ row("20260910"),
+ row("2026-09-10T00:00:00Z"),
+ row("2026-09-15"),
+ row("2020-01-01"),
+ row("2026-09-10", 0),
+ row("2026-09-10", -1),
+ row("2026-09-10", "NaN"),
+ row("2026-09-10", "Infinity"),
+ row("2026-09-10", True),
+ row("2026-09-10", None),
+ row("2026-09-10", "bad"),
+ "unexpected row",
+])
+@pytest.mark.asyncio
+async def test_invalid_upstream_rows_fail_closed(bad_row):
+ with pytest.raises(ValueError):
+ await provider_for(history() + [bad_row]).fx_move("SEK", "JPY")
+
+
+@pytest.mark.asyncio
+async def test_conflicting_duplicates_fail_closed():
+ payload = history()
+ with pytest.raises(ValueError, match="conflicting"):
+ await provider_for(payload + [{**payload[0], "rate": 2.0}]).fx_move("SEK", "JPY")
+
+
+@pytest.mark.parametrize("base,quote,days", [
+ ("SEK", "JPY", 1), ("SEK", "JPY", 91), ("SEK", "JPY", True),
+ ("SEK", "JPY", 7.0), ("SEK", "JPY", "7"), ("SEK", "SEK", 7),
+ ("sek", "SEK", 7), (" SEK", "JPY", 7), ("123", "JPY", 7),
+ (None, "JPY", 7), ("SEK", "JP", 7),
+])
+@pytest.mark.asyncio
+async def test_inputs_are_validated_before_io(base, quote, days):
+ requests = []
+ with pytest.raises(ValueError):
+ await provider_for([], requests).fx_move(base, quote, days)
+ assert requests == []
+
+
+@pytest.mark.asyncio
+async def test_http_error_is_preserved_and_empty_payload_rejected():
+ with pytest.raises(httpx.HTTPStatusError):
+ await provider_for({"message": "unavailable"}, status_code=503).fx_move("SEK", "JPY")
+ with pytest.raises(ValueError, match="Not enough"):
+ await provider_for([]).fx_move("SEK", "JPY")
+
+
+@pytest.mark.asyncio
+async def test_sparse_missing_horizon_endpoints_are_not_interpolated():
+ # Ten-calendar-day observations cannot provide exact seven-day returns.
+ payload = [row(date(2026, 1, 1) + timedelta(days=10 * index), 15 + index / 100) for index in range(25)]
+ payload += [row("2026-09-04", 15), row("2026-09-11", 14)]
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["anomaly"]["sample_count"] == 0
+ assert event.metadata["anomaly"]["fallback_reason"] == "insufficient_history"
+
+
+@pytest.mark.asyncio
+async def test_extreme_rates_cannot_produce_nonfinite_percentage():
+ with pytest.raises(ValueError, match="numeric range"):
+ await provider_for([row("2026-09-04", 1e-300), row("2026-09-11", 1e300)]).fx_move("SEK", "JPY")
+
+
+@pytest.mark.asyncio
+async def test_rolling_sample_count_is_bounded():
+ event = await provider_for(history(days=390)).fx_move("SEK", "JPY")
+ assert event.metadata["anomaly"]["sample_count"] == 252
+
+
+@pytest.mark.parametrize("sample_count", [29, 30])
+@pytest.mark.asyncio
+async def test_minimum_history_boundary(sample_count):
+ # Daily observations make the number of exact seven-day prior windows
+ # deterministic: observations minus the event and reference horizons.
+ count = sample_count + 14
+ start = date(2026, 9, 11) - timedelta(days=count - 1)
+ payload = [row(start + timedelta(days=index), 15 + math.sin(index / 3) / 10) for index in range(count)]
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["anomaly"]["sample_count"] == sample_count
+ assert ("z_score" in event.metadata) is (sample_count == 30)
+
+
+@pytest.mark.asyncio
+async def test_query_dates_use_utc_not_local_calendar_date():
+ provider = provider_for(history())
+ provider.now = lambda: datetime(2026, 9, 14, 1, tzinfo=timezone(timedelta(hours=3)))
+ event = await provider.fx_move("SEK", "JPY")
+ assert event.metadata["query_end_date"] == "2026-09-13"
+ assert event.metadata["latest_observation_age_days"] == 2
+
+
+@pytest.mark.parametrize("age_days", [14, 15])
+@pytest.mark.asyncio
+async def test_stale_latest_observation_guard_boundary(age_days):
+ latest = NOW.date() - timedelta(days=age_days)
+ payload = [row(latest - timedelta(days=7), 15), row(latest, 14)]
+ if age_days > 14:
+ with pytest.raises(ValueError, match="more than 14 calendar days old"):
+ await provider_for(payload).fx_move("SEK", "JPY")
+ else:
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["latest_observation_age_days"] == 14
+ assert any("14 calendar days old" in note for note in event.provenance.notes)
+
+
+@pytest.mark.parametrize("gap_days", [7, 8])
+@pytest.mark.asyncio
+async def test_missing_baseline_guard_boundary(gap_days):
+ latest = date(2026, 9, 11)
+ baseline = latest - timedelta(days=7 + gap_days)
+ payload = [row(baseline, 15), row(latest, 14)]
+ if gap_days > 7:
+ with pytest.raises(ValueError, match="more than 7 calendar days before"):
+ await provider_for(payload).fx_move("SEK", "JPY")
+ else:
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["anchor_gap_days"] == 7
+ assert event.metadata["actual_calendar_days"] == 14
+
+
+@pytest.mark.asyncio
+async def test_frankfurter_weekend_start_backfill_is_retained_and_disclosed():
+ # Observed live v2 behavior: from=2025-08-16 (Saturday) includes Friday.
+ payload = [row("2025-08-15", 15.3711), *history()]
+ requests = []
+ event = await provider_for(payload, requests).fx_move("SEK", "JPY")
+ assert requests[0].url.params["from"] == "2025-08-16"
+ assert event.metadata["query_start_date"] == "2025-08-16"
+ assert event.metadata["history_start_date"] == "2025-08-15"
+ assert event.metadata["pre_query_observation_count"] == 1
+ assert event.metadata["old_date"] == "2026-09-04"
+ assert event.metadata["new_date"] == "2026-09-11"
+
+
+@pytest.mark.parametrize("padding_days", [7, 8])
+@pytest.mark.asyncio
+async def test_start_backfill_is_bounded_to_seven_days(padding_days):
+ query_start = NOW.date() - timedelta(days=394)
+ payload = [row(query_start - timedelta(days=padding_days)), *history()]
+ if padding_days > 7:
+ with pytest.raises(ValueError, match="out-of-query"):
+ await provider_for(payload).fx_move("SEK", "JPY")
+ else:
+ event = await provider_for(payload).fx_move("SEK", "JPY")
+ assert event.metadata["pre_query_observation_count"] == 1
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_guardrails.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_guardrails.py
new file mode 100644
index 0000000..b51a98a
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_guardrails.py
@@ -0,0 +1,100 @@
+import math
+
+import pytest
+from pydantic import ValidationError
+
+from life_exchange_rate.calculations import _payment
+from life_exchange_rate.models import MacroEvent, ScenarioInput, ScenarioRequest, TranslateRequest
+from life_exchange_rate.providers.fixtures import demo_events, demo_profile
+from life_exchange_rate.service import compare_scenarios, translate_request
+
+
+def test_inverse_fx_trace_and_amount_match_forward_quote():
+ forward = demo_events()[0]
+ inverse_data = forward.model_dump()
+ inverse_data.update(base_currency="JPY", quote_currency="SEK", unit="SEK per JPY",
+ old_value=1 / forward.old_value, new_value=1 / forward.new_value, change_pct=None)
+ inverse = MacroEvent.model_validate(inverse_data)
+ result = translate_request(TranslateRequest(event=inverse, profile=demo_profile()))
+ direct = translate_request(TranslateRequest(event=forward, profile=demo_profile()))
+ assert result["direct_effect_home"] == direct["direct_effect_home"] == 1739.13
+ for step in result["calculation_trace"][:2]:
+ inputs = step["inputs"]
+ rate = inputs.get("old_fx_rate", inputs.get("new_fx_rate"))
+ assert round(inputs["travel_budget_home"] * rate, 2) == step["result"]
+
+
+def test_foreign_mortgage_is_not_counted_as_home_money():
+ profile = demo_profile()
+ profile.mortgage.currency = "USD"
+ profile.savings_balance = None
+ result = translate_request(TranslateRequest(event=demo_events()[1], profile=profile))
+ assert result["direct_effect_home"] is None
+ assert any("Foreign-currency mortgage" in warning for warning in result["warnings"])
+
+
+def test_home_savings_remain_eligible_when_foreign_mortgage_excluded():
+ profile = demo_profile()
+ profile.mortgage.currency = "USD"
+ result = translate_request(TranslateRequest(event=demo_events()[1], profile=profile))
+ assert result["direct_effect_home"] == -10.42
+ assert "mortgage_monthly_delta" not in result["details"]
+
+
+def test_unscoped_policy_cannot_drive_calculation():
+ event = demo_events()[1].model_copy(update={"affected_currencies": []})
+ result = translate_request(TranslateRequest(event=event, profile=demo_profile()))
+ assert result["direct_effect_home"] is None
+
+
+@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")])
+def test_nonfinite_market_values_rejected(value):
+ data = demo_events()[0].model_dump()
+ data["new_value"] = value
+ with pytest.raises(ValidationError):
+ MacroEvent.model_validate(data)
+
+
+def test_conflicting_percent_and_missing_structured_values_rejected():
+ data = demo_events()[2].model_dump()
+ data["change_pct"] = 100
+ with pytest.raises(ValidationError, match="agree"):
+ MacroEvent.model_validate(data)
+ with pytest.raises(ValidationError):
+ MacroEvent.model_validate({"title": "Rates rise", "requires_quantification": True})
+
+
+def test_scenarios_are_labeled_and_invalid_scaled_prices_rejected():
+ request = ScenarioRequest(event=demo_events()[0], profile=demo_profile(),
+ scenarios=[ScenarioInput(label="mild", multiplier=.5)])
+ result = compare_scenarios(request)[0]["result"]
+ assert result["confidence"] == "scenario"
+ request.scenarios = [ScenarioInput(label="invalid", multiplier=20)]
+ with pytest.raises(ValidationError, match="positive"):
+ compare_scenarios(request)
+
+
+def test_fixtures_are_stable_and_carry_provenance_to_result():
+ assert [e.model_dump() for e in demo_events()] == [e.model_dump() for e in demo_events()]
+ event = demo_events()[0]
+ result = translate_request(TranslateRequest(event=event, profile=demo_profile()))
+ assert result["event_provenance"]["source_type"] == "fixture"
+ assert result["confidence"] == "scenario"
+ assert any("synthetic" in warning for warning in result["warnings"])
+
+
+def test_payment_is_stable_near_zero_and_at_high_rates():
+ assert _payment(120000, 0, 10) == 1000
+ assert _payment(120000, 1e-12, 10) == pytest.approx(1000)
+ assert math.isfinite(_payment(120000, 100, 1000))
+
+
+def test_radar_does_not_claim_relevance_for_excluded_exposures():
+ from life_exchange_rate.radar import score_user_relevance
+ profile = demo_profile()
+ rate = demo_events()[1].model_copy(update={"affected_currencies": ["EUR"]})
+ assert score_user_relevance(rate, profile)[0] == 0
+ profile.commute.monthly_fuel_liters = 0
+ assert score_user_relevance(demo_events()[2], profile)[0] == 0
+ profile.travel_target_currency = "USD"
+ assert score_user_relevance(demo_events()[0], profile)[0] == 0
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_mcp.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_mcp.py
new file mode 100644
index 0000000..2979f38
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_mcp.py
@@ -0,0 +1,91 @@
+import pytest
+from mcp import Client
+
+from life_exchange_rate import mcp_server as server
+from life_exchange_rate.providers.fixtures import demo_events, demo_profile
+
+
+EXPECTED_TOOLS = {
+ "get_macro_events", "get_official_macro_headlines", "get_live_fx_event",
+ "get_policy_rate_event", "get_energy_event", "rank_events_for_user",
+ "translate_event_to_life", "convert_to_life_units", "compare_event_scenarios",
+}
+
+
+def data(result):
+ assert not result.is_error, result.content
+ assert result.structured_content is not None
+ structured = result.structured_content
+ return structured["result"] if set(structured) == {"result"} else structured
+
+
+@pytest.mark.asyncio
+async def test_mcp_discovery_calls_validation_and_errors(monkeypatch):
+ async def fixture_fx(*args, **kwargs):
+ return demo_events()[0]
+
+ async def fixture_headlines(*args, **kwargs):
+ return [{"headline_id": "test-headline", "title": "Fixture headline, not an observation", "requires_quantification": True}]
+
+ monkeypatch.setattr(server, "fetch_live_fx_event", fixture_fx)
+ monkeypatch.setattr(server, "get_official_headlines", fixture_headlines)
+ profile = demo_profile().model_dump(mode="json")
+ async with Client(server.mcp) as client:
+ listed = await client.list_tools()
+ assert {tool.name for tool in listed.tools} == EXPECTED_TOOLS
+ translate_schema = next(tool.input_schema for tool in listed.tools if tool.name == "translate_event_to_life")
+ assert "assumptions" in translate_schema["properties"]
+ events = data(await client.call_tool("get_macro_events", {"mode": "demo"}))
+ assert len(events) == 3
+ event = data(await client.call_tool("get_live_fx_event", {"base_currency": "SEK", "quote_currency": "JPY"}))
+ headlines = data(await client.call_tool("get_official_macro_headlines", {"source": "fed", "limit": 1}))
+ assert headlines[0]["requires_quantification"] is True
+ for tool_name in ("get_policy_rate_event", "get_energy_event"):
+ fixture = data(await client.call_tool(tool_name, {"mode": "fixture"}))
+ assert fixture["provenance"]["source_type"] == "synthetic_fixture"
+ ranked = data(await client.call_tool("rank_events_for_user", {"events": events, "profile": profile}))
+ assert len(ranked) == 3
+ assert ranked[0]["priority_score"] >= ranked[-1]["priority_score"]
+ impact = data(await client.call_tool("translate_event_to_life", {"event": event, "profile": profile}))
+ assert impact["direct_effect_home"] == 1739.13
+ assert impact["work_hours_equivalent"] == 8.7
+ assert impact["calculation_trace"]
+ conversion = data(await client.call_tool("convert_to_life_units", {"amount": 1739.13, "currency": "SEK", "profile": profile}))
+ assert conversion["work_hours_equivalent"] == 8.7
+ zero_pass_through = {"interest_rate_pass_through": 0, "oil_to_fuel_pass_through": 0}
+ oil_impact = data(await client.call_tool("translate_event_to_life", {
+ "event": events[2], "profile": profile, "assumptions": zero_pass_through,
+ }))
+ assert oil_impact["direct_effect_home"] == 0
+ scenarios = data(await client.call_tool("compare_event_scenarios", {
+ "event": events[2], "profile": profile, "assumptions": zero_pass_through,
+ }))
+ assert len(scenarios) == 3
+ assert all(item["result"]["direct_effect_home"] == 0 for item in scenarios)
+
+ invalid_calls = [
+ ("get_macro_events", {"mode": "invent"}),
+ ("get_live_fx_event", {"base_currency": "SEK", "quote_currency": "SEK"}),
+ ("get_live_fx_event", {"base_currency": "12!", "quote_currency": "JPY"}),
+ ("get_live_fx_event", {"base_currency": "SEK", "quote_currency": "JPY", "lookback_days": 1}),
+ ("get_official_macro_headlines", {"source": "untrusted"}),
+ ("get_official_macro_headlines", {"source": "fed", "limit": 51}),
+ ("get_policy_rate_event", {"mode": "invent"}),
+ ("get_energy_event", {"lookback_days": 366}),
+ ("translate_event_to_life", {"event": headlines[0], "profile": profile}),
+ ("translate_event_to_life", {"event": events[2], "profile": profile, "assumptions": {"oil_to_fuel_pass_through": 2}}),
+ ("convert_to_life_units", {"amount": 100, "currency": "USD", "profile": profile}),
+ ("compare_event_scenarios", {"event": event, "profile": profile, "mild_multiplier": 0}),
+ ]
+ for tool_name, arguments in invalid_calls:
+ result = await client.call_tool(tool_name, arguments)
+ assert result.is_error, tool_name
+
+ async def failed_fetch(*args, **kwargs):
+ raise RuntimeError("https://provider.invalid/?api_key=DO_NOT_LEAK")
+
+ monkeypatch.setattr(server, "fetch_energy_event", failed_fetch)
+ failed = await client.call_tool("get_energy_event", {"mode": "live"})
+ assert failed.is_error
+ assert "DO_NOT_LEAK" not in str(failed.content)
+ assert "provider.invalid" not in str(failed.content)
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_official_rss.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_official_rss.py
new file mode 100644
index 0000000..3465afd
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_official_rss.py
@@ -0,0 +1,21 @@
+from life_exchange_rate.models import EventType
+from life_exchange_rate.providers.official_rss import parse_feed
+
+
+SAMPLE = """
+
+-
+Federal Reserve issues FOMC statement on monetary policy
+https://example.test/fomc
+Thu, 10 Sep 2026 18:00:00 GMT
+
+
+"""
+
+
+def test_rss_parser_keeps_headline_separate_from_quantified_event():
+ rows = parse_feed(SAMPLE, source="fed")
+ assert len(rows) == 1
+ assert rows[0].event_type_hint == EventType.INTEREST_RATE_CHANGE
+ assert rows[0].requires_quantification is True
+ assert rows[0].publisher == "Federal Reserve Board"
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_radar.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_radar.py
new file mode 100644
index 0000000..4a067d1
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_radar.py
@@ -0,0 +1,23 @@
+from life_exchange_rate.providers.fixtures import demo_events, demo_profile
+import pytest
+
+from life_exchange_rate.radar import rank_events, score_market_significance
+
+
+def test_demo_radar_prioritizes_personally_relevant_large_events():
+ ranked = rank_events(demo_events(), demo_profile())
+ assert len(ranked) == 3
+ assert ranked[0].priority_score >= ranked[-1].priority_score
+ jpy = next(item for item in ranked if item.event.event_id == "demo-jpy-sek-8pct")
+ assert jpy.user_relevance == 100
+ assert jpy.market_significance >= 75
+ assert any("travel target" in reason for reason in jpy.relevance_reasons)
+
+
+@pytest.mark.parametrize("invalid_z", [float("nan"), float("inf"), float("-inf"), True, "3.2", None, 10**500])
+def test_invalid_metadata_z_score_uses_transparent_fallback(invalid_z):
+ event = demo_events()[0]
+ event.metadata["z_score"] = invalid_z
+ score, method = score_market_significance(event)
+ assert 0 <= score <= 100
+ assert method == "fallback_fx_percent_move"
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_structured_providers.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_structured_providers.py
new file mode 100644
index 0000000..51d311d
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_structured_providers.py
@@ -0,0 +1,216 @@
+"""Offline API-contract tests; every mocked observation below is synthetic."""
+import csv
+import io
+import json
+from datetime import date, datetime, timezone
+
+import httpx
+import pytest
+
+from life_exchange_rate.models import Confidence, TranslateRequest
+from life_exchange_rate.calculations import translate_event
+from life_exchange_rate.providers.common import ProviderError
+from life_exchange_rate.providers.energy import EnergyProvider
+from life_exchange_rate.providers.fixtures import demo_profile
+from life_exchange_rate.providers.policy_rate import PolicyRateProvider
+
+NOW = datetime(2026, 1, 12, 15, 30, tzinfo=timezone.utc)
+AS_OF = date(2026, 1, 12)
+
+
+def policy_csv(records=None, **overrides):
+ records = records if records is not None else [("2025-12-01", "2.00"), ("2025-12-11", "2.25"), ("2026-01-10", "2.50")]
+ content = io.StringIO()
+ writer = csv.DictWriter(content, fieldnames=["KEY", "TIME_PERIOD", "OBS_VALUE", "UNIT", "UNIT_MULT", "OBS_STATUS"])
+ writer.writeheader()
+ for day, value in records:
+ writer.writerow({"KEY": PolicyRateProvider.SERIES_ID, "TIME_PERIOD": day, "OBS_VALUE": value, "UNIT": "PCPA", "UNIT_MULT": "0", "OBS_STATUS": "A", **overrides})
+ return content.getvalue()
+
+
+def energy_payload(records=None, **overrides):
+ records = records if records is not None else [("2025-12-15", "75"), ("2026-01-02", "80"), ("2026-01-09", "88")]
+ rows = [{"period": day, "value": value, "series": "RBRTE", "units": "Dollars per Barrel", **overrides} for day, value in records]
+ return {"response": {"data": rows, "total": str(len(rows))}}
+
+
+def policy_provider(content, calls=None):
+ def handler(request):
+ if calls is not None:
+ calls.append(request)
+ return httpx.Response(200, text=content)
+ return PolicyRateProvider(transport=httpx.MockTransport(handler), today=lambda: AS_OF, now=lambda: NOW)
+
+
+def energy_provider(payload, calls=None, key="unit-test-key"):
+ def handler(request):
+ if calls is not None:
+ calls.append(request)
+ return httpx.Response(200, json=payload)
+ return EnergyProvider(api_key=key, transport=httpx.MockTransport(handler), today=lambda: AS_OF, now=lambda: NOW)
+
+
+@pytest.mark.asyncio
+async def test_ecb_uses_requested_horizon_scope_and_source_dates():
+ calls = []
+ event = await policy_provider(policy_csv(), calls).policy_rate_move(30, "live")
+ assert event.old_value == 2.25 # Not the first padding observation at 2.00.
+ assert event.new_value == 2.5
+ assert event.change_pct is None # Policy moves are percentage points, even at zero/negative rates.
+ assert event.metadata["change_basis_points"] == 25
+ assert event.metadata["actual_window_days"] == 30
+ assert event.metadata["observation_lag_days"] == 2
+ assert event.affected_currencies == ["EUR"] and event.jurisdiction == "EA"
+ assert event.observed_at.date() == date(2026, 1, 10)
+ assert event.provenance.retrieved_at == NOW
+ assert event.provenance.series_id == PolicyRateProvider.SERIES_ID
+ assert calls[0].url.params["startPeriod"] == "2025-11-15"
+ assert calls[0].url.params["endPeriod"] == AS_OF.isoformat()
+ assert calls[0].url.params["format"] == "csvdata"
+ impact = translate_event(TranslateRequest(event=event, profile=demo_profile()))
+ assert impact.direct_effect_home is None # Euro-area policy must not alter a SEK mortgage.
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("old,new", [("0", "0.25"), ("-0.5", "0"), ("2", "2")])
+async def test_policy_zero_negative_and_unchanged_levels_are_valid(old, new):
+ event = await policy_provider(policy_csv([("2025-12-11", old), ("2026-01-10", new)])).policy_rate_move(mode="live")
+ assert event.metadata["change_basis_points"] == (float(new) - float(old)) * 100
+ assert event.change_pct is None
+
+
+@pytest.mark.asyncio
+async def test_eia_queries_daily_brent_and_keeps_api_key_out_of_provenance():
+ calls = []
+ event = await energy_provider(energy_payload(), calls).oil_move(7, "live")
+ assert (event.old_value, event.new_value) == (80, 88)
+ assert event.change_pct == pytest.approx(10)
+ assert event.window_start.date() == date(2026, 1, 2)
+ assert event.window_end.date() == date(2026, 1, 9)
+ assert event.provenance.retrieved_at == NOW
+ assert event.provenance.series_id == "PET.RBRTE.D"
+ params = calls[0].url.params
+ assert params["frequency"] == "daily" and params["facets[series][]"] == "RBRTE"
+ assert params["start"] == "2025-12-08" and params["end"] == AS_OF.isoformat()
+ assert params["api_key"] == "unit-test-key"
+ assert "unit-test-key" not in event.model_dump_json()
+ assert "api_key" not in event.provenance.source_url
+
+
+@pytest.mark.asyncio
+async def test_weekend_baseline_uses_prior_published_day():
+ event = await energy_provider(energy_payload([("2026-01-02", "80"), ("2026-01-11", "88")])).oil_move(7, "live")
+ assert event.metadata["target_start_date"] == "2026-01-04"
+ assert event.metadata["actual_window_days"] == 9
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("provider,method", [(PolicyRateProvider, "policy_rate_move"), (EnergyProvider, "oil_move")])
+async def test_fixture_is_offline_deterministic_and_never_official(provider, method):
+ def forbidden_network(request):
+ raise AssertionError("fixture mode must make no network requests")
+ instance = provider(transport=httpx.MockTransport(forbidden_network), today=lambda: AS_OF, now=lambda: NOW)
+ event = await getattr(instance, method)(mode="fixture")
+ repeat = await getattr(instance, method)(mode="fixture")
+ assert event == repeat
+ assert event.metadata["synthetic"] is True
+ assert event.metadata["fallback_used"] is False
+ assert event.confidence == Confidence.SCENARIO
+ assert event.evidence_url is None and event.provenance.source_url is None
+ assert event.provenance.source_type == "synthetic_fixture"
+ assert event.observed_at.date() == date(2025, 12, 31)
+ full_year = await getattr(instance, method)(365, "fixture")
+ assert full_year.metadata["actual_window_days"] == 365
+
+
+@pytest.mark.asyncio
+async def test_missing_eia_key_has_explicit_fallback_and_live_fails():
+ provider = energy_provider({}, key="")
+ event = await provider.oil_move()
+ assert event.metadata["fallback_used"] is True
+ assert event.metadata["fallback_reason"] == "EIA_API_KEY is not configured"
+ assert event.change_pct == pytest.approx(15)
+ with pytest.raises(ProviderError, match="EIA_API_KEY is not configured"):
+ await provider.oil_move(mode="live")
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("status", [302, 401, 429, 500])
+async def test_http_failures_are_sanitized_and_fallback_labeled(status):
+ secret = "do-not-expose-this-key"
+ def fail(request):
+ return httpx.Response(status, text=f"Error at {request.url}; {secret}", headers={"Location": f"https://example.com/{secret}"})
+ provider = EnergyProvider(api_key=secret, transport=httpx.MockTransport(fail), now=lambda: NOW)
+ event = await provider.oil_move()
+ assert event.metadata["fallback_used"] is True
+ assert secret not in event.model_dump_json()
+ with pytest.raises(ProviderError) as error:
+ await provider.oil_move(mode="live")
+ assert secret not in str(error.value)
+ assert str(status) in str(error.value)
+
+
+@pytest.mark.asyncio
+async def test_transport_error_does_not_echo_request_url():
+ def fail(request):
+ raise httpx.ConnectError(f"private message {request.url}", request=request)
+ provider = EnergyProvider(api_key="hidden-key", transport=httpx.MockTransport(fail))
+ with pytest.raises(ProviderError, match="could not be reached") as error:
+ await provider.oil_move(mode="live")
+ assert "hidden-key" not in str(error.value)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("content", [
+ "not,csv\n1,2", policy_csv([], UNIT="PCPA"), policy_csv(UNIT="USD"), policy_csv(UNIT_MULT="2"),
+ policy_csv(KEY="FM.D.US.USD.INVALID"), policy_csv(OBS_STATUS="M"),
+ policy_csv([("2025-12-11", "2"), ("2026-01-10", "nan")]),
+ policy_csv([("2025-12-11", ""), ("2026-01-10", "2")]),
+ policy_csv([("2025-12-11", "2"), ("2026-01-10", "2"), ("2026-01-10", "3")]),
+ policy_csv([("2025-12-11", "2"), ("2026-01-13", "3")]),
+ policy_csv([("2025-10-01", "2"), ("2025-12-01", "3")]),
+ policy_csv([("2025-12-12", "2"), ("2026-01-10", "3")]),
+ policy_csv([("2025-12-11", "2"), ("2026-W02-1", "3")]),
+ policy_csv().replace("OBS_STATUS", "OBS_VALUE"),
+])
+async def test_ecb_rejects_missing_invalid_duplicate_future_stale_or_short_windows(content):
+ provider = policy_provider(content)
+ with pytest.raises(ProviderError):
+ await provider.policy_rate_move(mode="live")
+ assert (await provider.policy_rate_move()).metadata["fallback_used"] is True
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("payload", [
+ None, {}, {"error": "remote-key-sensitive-message"}, {"response": {"data": []}},
+ energy_payload(series="RWTC"), energy_payload(units="Dollars per Gallon"),
+ energy_payload([("2026-01-02", "0"), ("2026-01-09", "88")]),
+ energy_payload([("2026-01-02", "80"), ("2026-01-09", "inf")]),
+ energy_payload([("2026-01-02", "80"), ("2026-01-09", None)]),
+ energy_payload([("2026-01-02", "80"), ("2026-01-09", "88"), ("2026-01-09", "90")]),
+ energy_payload([("2026-01-02", "80"), ("2026-01-13", "88")]),
+ energy_payload([("2026-01-02", "80"), ("2026-W02-1", "88")]),
+ {"response": {"total": "5001", "data": energy_payload()["response"]["data"]}},
+])
+async def test_eia_rejects_bad_shapes_values_units_duplicates_and_truncation(payload):
+ provider = energy_provider(payload)
+ with pytest.raises(ProviderError):
+ await provider.oil_move(mode="live")
+ event = await provider.oil_move()
+ assert event.metadata["fallback_used"] is True
+ assert "remote-key-sensitive-message" not in event.model_dump_json()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("lookback", [0, 366, True, 2.5])
+async def test_bad_request_does_not_silently_fallback(lookback):
+ with pytest.raises(ValueError, match="lookback_days"):
+ await energy_provider({}).oil_move(lookback, "live_or_fixture")
+ with pytest.raises(ValueError, match="lookback_days"):
+ await policy_provider("").policy_rate_move(lookback, "fixture")
+
+
+@pytest.mark.asyncio
+async def test_unknown_mode_is_rejected():
+ with pytest.raises(ValueError, match="mode"):
+ await energy_provider({}).oil_move(mode="automatic")
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_verification.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_verification.py
new file mode 100644
index 0000000..867aaa9
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/tests/test_verification.py
@@ -0,0 +1,28 @@
+import json
+from pathlib import Path
+
+from life_exchange_rate.main import app
+from life_exchange_rate.models import TranslateRequest
+from life_exchange_rate.service import translate_request
+
+ROOT = Path(__file__).resolve().parents[1] / "verification"
+
+
+def test_saved_openapi_matches_running_contract():
+ assert json.loads((ROOT / "openapi.json").read_text(encoding="utf-8")) == app.openapi()
+
+
+def test_saved_fx_response_matches_actual_calculation():
+ request = TranslateRequest.model_validate_json((ROOT / "fixtures/fx-request.json").read_text(encoding="utf-8"))
+ expected = json.loads((ROOT / "fixtures/fx-response.json").read_text(encoding="utf-8"))
+ assert translate_request(request) == expected
+ assert expected["direct_effect_home"] == 1739.13
+ assert expected["work_hours_equivalent"] == 8.7
+
+
+def test_policy_fixture_preserves_currency_boundary():
+ request = TranslateRequest.model_validate_json((ROOT / "fixtures/policy-request.json").read_text(encoding="utf-8"))
+ result = translate_request(request)
+ assert request.event.affected_currencies == ["EUR"]
+ assert request.profile.home_currency == "SEK"
+ assert result["direct_effect_home"] is None
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/uv.lock b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/uv.lock
new file mode 100644
index 0000000..34c437a
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/uv.lock
@@ -0,0 +1,1357 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+resolution-markers = [
+ "python_full_version >= '3.14'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "(python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" },
+ { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" },
+ { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
+ { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
+ { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
+ { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
+ { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
+ { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+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 = "cryptography"
+version = "50.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" },
+ { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" },
+ { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" },
+ { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" },
+ { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" },
+ { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" },
+ { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" },
+ { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" },
+ { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" },
+ { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" },
+ { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" },
+ { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" },
+ { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" },
+ { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" },
+ { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" },
+ { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" },
+ { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.141.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
+]
+
+[[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 = "httpcore2"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "h11" },
+ { name = "truststore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
+]
+
+[[package]]
+name = "httptools"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" },
+ { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
+ { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
+ { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
+ { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
+ { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
+ { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
+]
+
+[[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 = "httpx2"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio", marker = "sys_platform != 'emscripten'" },
+ { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
+ { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
+ { name = "idna" },
+ { name = "truststore", marker = "sys_platform != 'emscripten'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
+]
+
+[[package]]
+name = "httpx2-jsfetch"
+version = "1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
+]
+
+[[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 = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "life-exchange-rate-mcp"
+version = "0.2.0"
+source = { editable = "." }
+dependencies = [
+ { name = "fastapi" },
+ { name = "httpx" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "pydantic" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastapi", specifier = ">=0.128,<1" },
+ { name = "httpx", specifier = ">=0.28,<1" },
+ { name = "mcp", extras = ["cli"], specifier = ">=2.0.0,<3" },
+ { name = "pydantic", specifier = ">=2.13,<3" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=9,<10" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.2,<2" },
+ { name = "uvicorn", extras = ["standard"], specifier = ">=0.48,<1" },
+]
+provides-extras = ["dev"]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx2" },
+ { name = "jsonschema" },
+ { name = "mcp-types" },
+ { name = "opentelemetry-api" },
+ { name = "pydantic" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/31/ac54fb0fdd5b37de704486e288bba4fbbb463f24cfcfedbede407b854513/mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd", size = 4084129, upload-time = "2026-09-07T16:06:23.439Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/ff/8e7eade68b8a28f7da0ed1085544341b51f9c935dbf6b95c76b7edfea6a0/mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81", size = 365656, upload-time = "2026-09-07T16:06:19.711Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mcp-types"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/91/762d7755d971aff8a28d75f7961656148edf27875c8026e6385aaab08ae7/mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad", size = 65892, upload-time = "2026-09-07T16:06:25.187Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.44.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[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 = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.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/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" },
+ { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" },
+ { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" },
+ { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" },
+ { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" },
+ { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" },
+ { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" },
+ { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
+ { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
+ { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
+ { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
+ { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
+ { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
+ { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
+ { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
+ { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
+ { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
+ { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
+ { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
+ { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" },
+ { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" },
+ { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" },
+ { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" },
+ { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" },
+ { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177, upload-time = "2026-09-11T13:11:54.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896, upload-time = "2026-09-11T13:11:53.409Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[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 = "python-dotenv"
+version = "1.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.32"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" },
+ { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
+ { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
+ { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
+]
+
+[[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 = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" },
+ { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
+ { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
+ { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
+ { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
+ { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
+ { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
+ { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
+ { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
+ { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
+ { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" },
+ { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" },
+ { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.4.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size = 34972, upload-time = "2026-09-05T12:11:04.607Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size = 17122, upload-time = "2026-09-05T12:11:03.195Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
+]
+
+[[package]]
+name = "truststore"
+version = "0.10.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.27.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.53.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
+ { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
+ { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
+ { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
+ { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
+ { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
+ { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
+ { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
+ { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" },
+ { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" },
+ { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" },
+ { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" },
+ { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" },
+ { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" },
+ { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" },
+ { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" },
+ { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
+ { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
+ { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
+ { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
+ { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
+ { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
+ { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
+ { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
+ { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
+ { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
+ { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
+ { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
+ { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
+ { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
+ { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "17.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/ad/66a74d42fb537bd44056483eae6cbb7ebb10b742c300a0bf8cee427556d4/websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c", size = 216984, upload-time = "2026-08-26T14:55:20.747Z" },
+ { url = "https://files.pythonhosted.org/packages/70/1b/344ab22cea729e872f759b926441f7b822ab6cd106db527736afc066927f/websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9", size = 214667, upload-time = "2026-08-26T14:55:22.298Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/42/bace574b6ae80e1a8d6935b8c5f03fb67236233ec572e976fe826ff719cf/websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86", size = 214944, upload-time = "2026-08-26T14:55:23.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/87/08e35ca4a0ffafb500a16ff461bf9561ad2b755362adb5d077d4dba9affc/websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d", size = 224004, upload-time = "2026-08-26T14:55:24.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/00ae2e147eaa086fe8bdddd36f57216ce72b9a9dfc0b17c717005ebdacaf/websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695", size = 224278, upload-time = "2026-08-26T14:55:26.016Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e2/7aeb4e00defa68826f449392922a382ce7fdf542fe52190558dc1714e284/websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134", size = 225511, upload-time = "2026-08-26T14:55:27.183Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e4/655be3d93c3edbe1a51606073b5454ea6b1b32d87aa26253a6df952417b7/websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7", size = 228802, upload-time = "2026-08-26T14:55:28.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/33/98549a2afa9d68fe1b5a8e0a61cd461a43ea1ab7209bce675eea67c79190/websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6", size = 226075, upload-time = "2026-08-26T14:55:29.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/6b/cbc27e014d6c292b9b2709cfd32781a2b61eb30cd4c9130e7c57e41a204a/websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750", size = 224846, upload-time = "2026-08-26T14:55:30.964Z" },
+ { url = "https://files.pythonhosted.org/packages/10/a6/e57925f7a423d90f24559e85bac21a7f0b44c0cf4a5c0babc0759ca54bab/websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8", size = 222136, upload-time = "2026-08-26T14:55:32.376Z" },
+ { url = "https://files.pythonhosted.org/packages/32/07/b9de0400addb542ba7c819022abc3afa46cd7e518068881bceadac69d995/websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098", size = 225000, upload-time = "2026-08-26T14:55:33.527Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/bb/af2828a1d7f2beb792af6ba56d7b02d56262070266b95d2af9ef391fbfb0/websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a", size = 223592, upload-time = "2026-08-26T14:55:34.636Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/51/7379f254730c1dc7d8e4dd8d686868d8f7be55bdc94c6d3a44538840f639/websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c", size = 224360, upload-time = "2026-08-26T14:55:35.777Z" },
+ { url = "https://files.pythonhosted.org/packages/88/c6/fcd91320dd71dda7046df9bc60f60c70ee15c052dee21e31ed6221dc8b5d/websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405", size = 225404, upload-time = "2026-08-26T14:55:36.85Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/b2/655a4f939388079f80f1b3f8a1b9d40783e70a376e7423988cc9a590a09f/websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66", size = 222982, upload-time = "2026-08-26T14:55:38.006Z" },
+ { url = "https://files.pythonhosted.org/packages/87/75/37c84c4371c6aa668910d7841036c4397ea10554c07030603ccd5e44b02a/websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0", size = 224017, upload-time = "2026-08-26T14:55:39.39Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/20/8a9a94323bfcfe03bde3f9d98926bea4855856359702fe3ca0d07051ef5d/websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb", size = 224252, upload-time = "2026-08-26T14:55:40.498Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/dd/aa66e6500188cd40306abeb92c9a738ca6dd7029d8d8532c538055ab5daf/websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142", size = 217484, upload-time = "2026-08-26T14:55:41.763Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a2/cdf3b551f0b9177023afd3a45d3b431a0d4064951008c4321a8b42ac2288/websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee", size = 217779, upload-time = "2026-08-26T14:55:42.942Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/13/51253dbed7d16a4bb87b05110ad3bf12165f410e915f6da1edd4186d8dc1/websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c", size = 217710, upload-time = "2026-08-26T14:55:44.016Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" },
+ { url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e7/df821761772beaa48c211ee0e234930b35c1473778470773823f56d3911b/websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab", size = 214959, upload-time = "2026-08-26T14:55:47.885Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/92/c3fb72f11764812fc648bf3838d224972427b348e8b3989d9e0a9df87da3/websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74", size = 224278, upload-time = "2026-08-26T14:55:49.241Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/05/9f82d090c8d2d861604147ef6dfb938a90b039f9358d5193f1df62558593/websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6", size = 224557, upload-time = "2026-08-26T14:55:50.348Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/50/5cbf677b865290fe36819ff00615826e7edc1df38786f770123ff39a933d/websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08", size = 225791, upload-time = "2026-08-26T14:55:51.75Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/1c/eb8a032285243381b09a221ae384c972d5000453ad136add4d1595cec798/websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11", size = 228574, upload-time = "2026-08-26T14:55:52.922Z" },
+ { url = "https://files.pythonhosted.org/packages/69/85/413736251cb3ac04ce84cbd90e893d9a36a9698d4820b323aff3aa187e50/websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4", size = 226428, upload-time = "2026-08-26T14:55:54.263Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/2b/a08bcc7fa1ca81a10f84ba32b6e6edd73a913f4b0c2640eed1fd626efacd/websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8", size = 225184, upload-time = "2026-08-26T14:55:55.943Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/8a/3bd2d0cf6b148c8c866d5d9fdcde30c04bfd81fdfac86813e69377eb4448/websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391", size = 222430, upload-time = "2026-08-26T14:55:57.103Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c9/8e891ae342668735eabbbc669895e15195e4b45f24a4beeb58af76f414c7/websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a", size = 225227, upload-time = "2026-08-26T14:55:58.375Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/6f/c816f332dca11425e9bda7c07f7573eb5c5f8a735849d02b0d81e8ee20fa/websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837", size = 223831, upload-time = "2026-08-26T14:55:59.664Z" },
+ { url = "https://files.pythonhosted.org/packages/53/67/5e91d5308ce24fc1ec74f56536c12f4888bad45ff5ea50f3180f8c518c57/websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641", size = 224600, upload-time = "2026-08-26T14:56:00.873Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/96/faa298ecf2570d35b0eb37caddf4992178d907e108ed74bfffb6bc092c29/websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090", size = 225707, upload-time = "2026-08-26T14:56:02.001Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/12/5710d2482ca5061c1eec5eb46f6313837c760d4115b1795c85b6c08be4e3/websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1", size = 223263, upload-time = "2026-08-26T14:56:03.092Z" },
+ { url = "https://files.pythonhosted.org/packages/27/47/0c30f4eebfd1d93fae779d268f678d48847fb98516f5200849574eee8820/websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067", size = 224244, upload-time = "2026-08-26T14:56:04.321Z" },
+ { url = "https://files.pythonhosted.org/packages/41/33/46c256195a1255079ae23d1b1267b2e1843dc5f46a67f973cdf2a3523dff/websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60", size = 224520, upload-time = "2026-08-26T14:56:05.521Z" },
+ { url = "https://files.pythonhosted.org/packages/06/9a/aef0792731df4352e5f417369b532b3325fe434765ca90c193f594ae1e67/websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb", size = 217485, upload-time = "2026-08-26T14:56:06.715Z" },
+ { url = "https://files.pythonhosted.org/packages/50/23/493ecfdaf32898e5ea24dc900e33e5e317f9662d5d9ab2d44b2e111b4e1c/websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec", size = 217786, upload-time = "2026-08-26T14:56:08.055Z" },
+ { url = "https://files.pythonhosted.org/packages/97/3d/91954e2f7876f74ce1213e9b92c65a63b559cc4b942a931ebeb351cd9932/websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729", size = 217711, upload-time = "2026-08-26T14:56:09.267Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" },
+ { url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" },
+ { url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" },
+ { url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" },
+ { url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" },
+ { url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" },
+ { url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" },
+ { url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" },
+ { url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" },
+ { url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" },
+ { url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" },
+ { url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" },
+ { url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" },
+ { url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" },
+ { url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" },
+ { url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" },
+ { url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" },
+ { url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" },
+ { url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" },
+ { url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" },
+ { url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" },
+ { url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" },
+ { url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" },
+ { url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" },
+ { url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" },
+ { url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" },
+ { url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" },
+ { url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" },
+ { url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" },
+ { url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" },
+ { url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" },
+ { url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" },
+ { url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" },
+ { url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" },
+ { url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/e4/af4abbcf07eac6a725ec6f865611526b2b0c23d482723de551bec667880d/websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918", size = 214602, upload-time = "2026-08-26T14:58:25.211Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/fe/819fba7ba35f92b639333da7355041c07dd50048f9c76fba0b8e292a6483/websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886", size = 214874, upload-time = "2026-08-26T14:58:26.689Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/a7/d370ab794f47fbeea648d17ad08caf0bb50131d6c04b7ad83e6af63c405a/websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8", size = 215821, upload-time = "2026-08-26T17:25:23.616Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/6b/251b00fe634e2a9c2cb5d6390e0e97cec55e3d18dd09b4b976620eed5d7b/websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf", size = 215714, upload-time = "2026-08-26T17:25:26.295Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b1/37fe0c96c206b4208a072c3a74add6a72af4b8228be3f5435163c5a6d099/websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e", size = 216608, upload-time = "2026-08-26T17:25:28.134Z" },
+ { url = "https://files.pythonhosted.org/packages/be/7e/75a0a491b512412e08333b9f8412757af6186fe1c598186261002de1a793/websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d", size = 217870, upload-time = "2026-08-26T17:25:29.745Z" },
+ { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" },
+]
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/vercel.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/vercel.json
new file mode 100644
index 0000000..cafc243
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/vercel.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "framework": "fastapi",
+ "functions": {"app.py": {"maxDuration": 30}}
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/MCP_SMOKE.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/MCP_SMOKE.md
new file mode 100644
index 0000000..7ebc2ef
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/MCP_SMOKE.md
@@ -0,0 +1,55 @@
+# MCP v2 verification
+
+## SDK and mount
+
+Verified against installed **mcp 2.2.0**, the current stable version shown on [PyPI](https://pypi.org/project/mcp/), on 2026-09-14.
+
+The server imports `MCPServer` from `mcp.server`. Transport settings belong on `streamable_http_app()`. FastAPI's parent lifespan starts `mcp.session_manager.run()`, because mounting a child ASGI application does not start its lifespan. `app.mount("/mcp", ...)` with `streamable_http_path="/"` serves the canonical endpoint **`/mcp/`**. These match the [official migration guide](https://py.sdk.modelcontextprotocol.io/migration/) and [ASGI mounting documentation](https://py.sdk.modelcontextprotocol.io/run/asgi/).
+
+The public hostname and origin are allowlisted from `PUBLIC_BASE_URL`; local development hosts are also accepted. Other hosts/origins are rejected. The application version is authoritative in `/health`, verification metadata and MCP server identity; health and verification return the same `REVIEW_COMMIT`. `dev-local` remains explicit until a real review commit is supplied. [Official deployment guidance](https://py.sdk.modelcontextprotocol.io/run/deploy/)
+
+## Executed live smoke
+
+At **2026-09-14 15:02:21 UTC**, `verification/smoke_mcp.py --live-fx` completed successfully against a real Uvicorn subprocess serving the FastAPI application at `http://127.0.0.1:53644/mcp/`. The official SDK `Client` connected over Streamable HTTP; this run used neither an in-memory transport nor mocked MCP responses. The script confirmed the server stopped after completion. Full records are in [mcp-smoke.json](mcp-smoke.json) and [end-to-end-demo.json](end-to-end-demo.json).
+
+| Client mode | Negotiated protocol | Tool discovery | Positive calls | Invalid calls |
+| --- | --- | --- | --- | --- |
+| `auto` | `2026-07-28` | All 9 tools | All 9 tools; 12 calls passed | 7 rejected as expected |
+| `legacy` | `2025-11-25` | All 9 tools | All 9 tools; 12 calls passed | 7 rejected as expected |
+
+The positive calls covered live Federal Reserve RSS, live ECB-filtered Frankfurter FX observations, synthetic macro events, policy-rate and energy fixture modes, ranking, translation, life-unit conversion, and explicit zero-pass-through scenarios. It also verified the fixed Japan-travel example: **20,000 SEK**, **15.0 → 13.8 JPY per SEK**, **1,739.13 SEK** extra budget and **8.70 work hours**. Income and unit prices are explicitly synthetic profile inputs.
+
+Negative checks covered unsupported modes, unknown headline sources, identical FX currencies, out-of-range lookback, headline-only translation, mismatched conversion currency and invalid scenario multipliers. `auto` and `legacy` use the [official client's protocol negotiation modes](https://py.sdk.modelcontextprotocol.io/protocol-versions/).
+
+## Recorded official-event demo
+
+The retained run retrieved **16.1127 → 15.8248 JPY per SEK**, observed on **2026-09-07 → 2026-09-14**. The observed move was **-1.7868%**, with a signed anomaly score of **-1.6794** from 252 preceding reference windows. Source URLs, source dates, retrieval time and anomaly statistics are retained in the event and impact records.
+
+The FX event received priority **73.9**, position **2** among the observed FX event and two explicitly synthetic comparison events. The unmatched EUR policy event has user relevance zero. Applying the supplied **20,000 SEK** travel budget produced:
+
+- **363.86 SEK** additional budget to preserve the original JPY purchasing power;
+- **1.82 work hours**, using supplied monthly income of 32,000 SEK and 160 work hours;
+- **4.55 beers**, **8.09 coffees**, or **2.60 lunches**, at supplied prices of 80, 45 and 140 SEK.
+
+The trace evaluates `20000 × 16.1127 / 15.8248 − 20000`. Retail fees and spreads are excluded. Personal income and local prices are explicitly illustrative inputs, not actual user finances or market price measurements.
+
+The separately fetched Fed headline was *Minutes of the Board's discount rate meetings on July 20 and July 29, 2026*, published `2026-08-25T18:00:00Z`. It retains `requires_quantification=true` and was rejected as a translation input. No causal link to the FX move is asserted: only independent structured observations drive the calculation.
+
+An earlier offline run passed both modes with seven positive fixture/calculation calls and seven expected input rejections each. The retained JSON artifacts now contain the later live successful run.
+
+## Reproduce
+
+```console
+uv run python verification/smoke_mcp.py
+uv run python verification/smoke_mcp.py --live-fx
+```
+
+The first command runs the reproducible flow. The second additionally makes positive official RSS and ECB-filtered live FX calls in both protocol modes, then records **official numeric FX event → ranked radar → JPY/SEK trip impact → work/life units**. The separately fetched Fed headline is contextual; the record expressly avoids asserting that it caused the FX move.
+
+Results are written to `verification/mcp-smoke.json` and `verification/end-to-end-demo.json`; `--output-dir` can preserve an additional run. Live failures record a redacted nested traceback and produce a nonzero exit code. The script reports skipped live calls explicitly and never substitutes synthetic data for a claimed live FX observation.
+
+## Regression checks
+
+The original dependency-installed baseline was **7 passed, 1 failed**: two test contexts attempted to restart one single-use SDK session manager. API tests now share one application lifespan. MCP tests additionally exercise all nine tools through the official SDK, input rejection, explicit pass-through settings, and sanitized provider errors. REST tests cover canonical discovery metadata, provider modes, headline rejection, and Host/Origin restrictions.
+
+The final regression suite passes 145 tests. Policy-rate ranking now excludes unmatched currency exposures, and malformed nonfinite JSON inputs return a safe 422 validation response.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/README.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/README.md
new file mode 100644
index 0000000..cdecd21
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/README.md
@@ -0,0 +1,46 @@
+# Verification runbook
+
+```powershell
+uv sync --locked --extra dev
+uv run --frozen python -m pytest
+uv run --frozen python verification/smoke_mcp.py
+uv run --frozen python verification/smoke_mcp.py --live-fx
+uv run --frozen python verification/generate_fixtures.py
+```
+
+The smoke script owns a temporary loopback Uvicorn process and stops it on success/failure. Default mode proves the synthetic MCP flow without external requests. `--live-fx` additionally exercises actual official RSS and FX; a live failure is recorded as failure, never as a successful fallback. `MCP_SMOKE.md` describes the latest retained run; machine evidence is in `mcp-smoke.json` and `end-to-end-demo.json`.
+
+`generate_fixtures.py` regenerates OpenAPI, the fixed FX request/response, invalid input and policy/energy fixture requests/responses, and the offline ranked demo. `tests/test_verification.py` checks the saved contract and fixed core response. Provider fixture dates are synthetic; captured live source dates and retrieval times remain distinct.
+
+Run the app with `uv run --frozen uvicorn life_exchange_rate.main:app --app-dir source --port 8000`. Check `/health`, `/.well-known/xagent-verification.json`, `/openapi.json`, `/v1/events/demo`, `/v1/radar/demo`, and `/mcp/` using a real MCP client. A raw browser GET is not a tool call. POST `fixtures/fx-request.json` to `/v1/translate` and compare the response to `fixtures/fx-response.json`.
+
+## Hosted deployment and revision binding
+
+The public deployment is **https://life-exchange-rate-mcp.vercel.app**, sourced from [KongkouKK/life-exchange-rate-mcp](https://github.com/KongkouKK/life-exchange-rate-mcp). Read `/health` and `/.well-known/xagent-verification.json` for the currently deployed source revision. Vercel Git deployments use the actual `VERCEL_GIT_COMMIT_SHA` and production domain automatically. Self-hosted deployments can instead set `REVIEW_COMMIT` to the reviewed Git SHA and `PUBLIC_BASE_URL` to the public origin. Do not submit `dev-local` or invented deployment URLs.
+
+Health and proof share package version and commit. Finalize `submission.json` and `SUBMISSION.md` from the verified deployed values and rerun hosted checks after deploying any source change. The MCP host allowlist derives from the resolved public origin while retaining loopback for local use. `RIGHTS.md` still requires the submitter's declarations; no official contest pull request has been submitted.
+
+The initial hosted run on 2026-09-16 at 06:30 UTC passed health and proof checks without redirects, within a 10-second timeout, and with matching revision, origin, slug, and MCP URL. MCP SDK 2.2.0 successfully initialized protocol versions `2026-07-28` and `2025-11-25`; each listed all nine tools and passed calls to `get_macro_events`, `translate_event_to_life`, and `convert_to_life_units`. Invalid event and conversion inputs were rejected. The fixture result was 1,739.13 SEK / 8.70 work hours with a calculation trace. This initial hosted run used only synthetic data and did not test live providers or call all nine tools.
+
+The subsequent [complete hosted run at 06:40 UTC](hosted-live-2026-09-16.json) passed both protocol versions. Each mode made 12 successful calls covering **all nine tools** and rejected **seven invalid inputs**: unsupported event mode, unknown headline source, identical FX currencies, out-of-range lookback, a headline lacking numeric event values, conversion currency mismatch, and an invalid scenario multiplier. Live ECB/Frankfurter FX and Fed RSS calls succeeded; policy-rate and energy calls explicitly used fixture mode. Health and deployment proof matched the expected source revision before and after the calls. The saved JSON is bound to its recorded historical revision; the current deployed revision must be read from the public proof.
+
+That run's observed FX window was 2026-09-08 through 2026-09-15, with 16.0689 → 15.8648 JPY/SEK. The explicit synthetic profile's 20,000 SEK travel budget required an additional 257.30 SEK, equivalent to 1.29 work hours, 5.72 coffees, or 1.84 lunches at the supplied income and prices. The record preserves the inputs, source dates, retrieval time, ranking, calculation trace, and unit conversion. The separately fetched Fed headline is not claimed to cause the FX move; only numeric FX observations drive that calculation.
+
+The full application, including deployment configuration, passed 148 local tests. Earlier retained `summary.json`, `build-check.json`, and local MCP records describe their own historical runs; they do not by themselves prove hosted behavior.
+
+EIA live validation requires `EIA_API_KEY`. Automated EIA success/error cases use mocked official-shaped responses; missing-key fallback is tested explicitly. No live EIA result should be claimed unless a keyed request was actually run.
+
+
+## Hosted review calls
+
+Run from the project root. These commands reproduce the checks; their presence is not a saved execution result:
+
+```bash
+API_ORIGIN='https://life-exchange-rate-mcp.vercel.app'
+curl --fail --silent --show-error "$API_ORIGIN/health"
+curl --fail --silent --show-error "$API_ORIGIN/.well-known/xagent-verification.json"
+curl --fail --silent --show-error "$API_ORIGIN/v1/translate" --header 'Content-Type: application/json' --data-binary @verification/fixtures/fx-request.json
+curl --silent --show-error --write-out '\nHTTP %{http_code}\n' "$API_ORIGIN/v1/translate" --header 'Content-Type: application/json' --data-binary @verification/fixtures/invalid-request.json
+```
+
+The capability fixture expects 1,739.13 SEK / 8.70 work hours. The invalid request expects HTTP 422. Connect a real MCP client to `https://life-exchange-rate-mcp.vercel.app/mcp/`, with the trailing slash, to check initialization and tools; a browser GET is not a tool call. Capture fresh hosted evidence after each reviewed deployment; localhost records alone are insufficient. In Windows PowerShell, set `$API_ORIGIN = 'https://life-exchange-rate-mcp.vercel.app'` and use `curl.exe` when `curl` is aliased.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/build-check.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/build-check.json
new file mode 100644
index 0000000..1ef9f4d
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/build-check.json
@@ -0,0 +1,11 @@
+{
+ "status": "passed",
+ "wheel": "life_exchange_rate_mcp-0.2.0-py3-none-any.whl",
+ "sha256": "3a4b358a61583d5af31d6d6fd7e21167da069f130f9076b1e9d1248a7ad35664",
+ "fixture_paths": [
+ "life_exchange_rate/providers/data/policy_rate.json",
+ "life_exchange_rate/providers/data/energy.json"
+ ],
+ "packaged_fixture_execution": "passed for both providers",
+ "docker_build": "not_run_docker_unavailable"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/end-to-end-demo.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/end-to-end-demo.json
new file mode 100644
index 0000000..93eaf7f
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/end-to-end-demo.json
@@ -0,0 +1,453 @@
+{
+ "event_mode": "live_official_observations",
+ "profile_origin": "Explicit synthetic judging profile; prices and income are inputs, not claimed market observations.",
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.79% from 2026-09-07 to 2026-09-14",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-14T00:00:00Z",
+ "old_value": 16.1127,
+ "new_value": 15.8248,
+ "unit": "JPY per SEK",
+ "change_pct": -1.7867893028480686,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-07T00:00:00Z",
+ "window_end": "2026-09-14T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-14T15:02:25.065700Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-07 through 2026-09-14.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-07",
+ "new_date": "2026-09-14",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-07",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 0,
+ "query_start_date": "2025-08-16",
+ "query_end_date": "2026-09-14",
+ "history_start_date": "2025-08-15",
+ "pre_query_observation_count": 1,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-28",
+ "reference_window_end": "2026-09-07",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.018029451191580836,
+ "reference_mean_log_return": 0.0007798034164666606,
+ "reference_sample_stddev": 0.011199713290856535,
+ "z_score": -1.6794407249160035,
+ "fallback_reason": null
+ },
+ "z_score": -1.6794407249160035
+ }
+ },
+ "ranked_radar": [
+ {
+ "event": {
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-14T15:02:24.415780Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-14"
+ }
+ },
+ "market_significance": 90.0,
+ "user_relevance": 100.0,
+ "priority_score": 95.5,
+ "significance_method": "fallback_oil_percent_move",
+ "relevance_reasons": [
+ "User supplied monthly fuel use and pump price",
+ "User has direct driving exposure"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.79% from 2026-09-07 to 2026-09-14",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-14T00:00:00Z",
+ "old_value": 16.1127,
+ "new_value": 15.8248,
+ "unit": "JPY per SEK",
+ "change_pct": -1.7867893028480686,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-07T00:00:00Z",
+ "window_end": "2026-09-14T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-14T15:02:25.065700Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-07 through 2026-09-14.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-07",
+ "new_date": "2026-09-14",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-07",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 0,
+ "query_start_date": "2025-08-16",
+ "query_end_date": "2026-09-14",
+ "history_start_date": "2025-08-15",
+ "pre_query_observation_count": 1,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-28",
+ "reference_window_end": "2026-09-07",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.018029451191580836,
+ "reference_mean_log_return": 0.0007798034164666606,
+ "reference_sample_stddev": 0.011199713290856535,
+ "z_score": -1.6794407249160035,
+ "fallback_reason": null
+ },
+ "z_score": -1.6794407249160035
+ }
+ },
+ "market_significance": 42.0,
+ "user_relevance": 100.0,
+ "priority_score": 73.9,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "FX pair includes the user's home currency",
+ "FX pair includes the user's travel target currency",
+ "User supplied a travel budget"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "event_type": "interest_rate_change",
+ "title": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 3.0,
+ "new_value": 3.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": null,
+ "window_start": "2025-12-01T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-14T15:02:24.409255Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-01",
+ "old_date": "2025-12-01",
+ "new_date": "2025-12-31",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "change_basis_points": 25.0,
+ "change_percentage_points": 0.25,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-14"
+ }
+ },
+ "market_significance": 45.0,
+ "user_relevance": 0.0,
+ "priority_score": 20.2,
+ "significance_method": "fallback_policy_rate_basis_points",
+ "relevance_reasons": [
+ "Policy-rate currency scope does not support a direct home-currency impact"
+ ]
+ }
+ ],
+ "impact": {
+ "event_provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-14T15:02:25.065700Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-07 through 2026-09-14.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "event_observed_at": "2026-09-14T00:00:00Z",
+ "event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "headline": "SEK/JPY moved -1.79% from 2026-09-07 to 2026-09-14",
+ "direct_effect_home": 363.86,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 1.82,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 4.55,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 8.09,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 2.6,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 363.86 SEK more than before.",
+ "confidence": "observed",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.1127
+ },
+ "result": 322254.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 15.8248
+ },
+ "result": 316496.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.1127,
+ "new_fx_rate": 15.8248
+ },
+ "result": 20363.86
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 20363.86,
+ "travel_budget_home": 20000.0
+ },
+ "result": 363.86
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 322254.0,
+ "new_target_purchasing_power": 316496.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ },
+ "life_unit_conversion": {
+ "amount": 363.86,
+ "currency": "SEK",
+ "work_hours_equivalent": 1.82,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 4.55,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 8.09,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 2.6,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ]
+ },
+ "separately_fetched_official_headline": {
+ "headline_id": "fed-9014ded4c9180ac8",
+ "title": "Minutes of the Board's discount rate meetings on July 20 and July 29, 2026",
+ "publisher": "Federal Reserve Board",
+ "published_at": "2026-08-25T18:00:00Z",
+ "url": "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260825a.htm",
+ "jurisdiction": "US",
+ "event_type_hint": null,
+ "tags": [],
+ "requires_quantification": true,
+ "source_type": "official_rss"
+ },
+ "headline_relationship": "Shown to test the headline boundary; no causal link to the FX move is asserted. Only the numeric FX observations drive this calculation.",
+ "recorded_at": "2026-09-14T15:02:21.201319+00:00",
+ "review_commit": "dev-local"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-event.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-event.json
new file mode 100644
index 0000000..3b7c429
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-event.json
@@ -0,0 +1,49 @@
+{
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-10"
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-request.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-request.json
new file mode 100644
index 0000000..5b15dfb
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-request.json
@@ -0,0 +1,91 @@
+{
+ "event": {
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-10"
+ }
+ },
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "assumptions": {
+ "interest_rate_pass_through": 0.5,
+ "oil_to_fuel_pass_through": 0.25
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-response.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-response.json
new file mode 100644
index 0000000..5eaeaa1
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/energy-response.json
@@ -0,0 +1,85 @@
+{
+ "event_provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "event_observed_at": "2025-12-31T00:00:00Z",
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "headline": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "direct_effect_home": 45.09,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "monthly_scenario",
+ "work_hours_equivalent": 0.23,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 0.56,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 1.0,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 0.32,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "Under the selected pass-through scenario, monthly fuel spending changes by about 45.09 SEK.",
+ "confidence": "scenario",
+ "assumptions": [
+ "25% of the crude-oil percentage move is applied to the user's pump price as a scenario."
+ ],
+ "warnings": [
+ "Crude-oil prices do not translate one-for-one into retail fuel prices.",
+ "Taxes, refining, distribution, inventories, currency moves, and timing can materially change pass-through.",
+ "This event uses a synthetic judging fixture, not a current market observation."
+ ],
+ "calculation_trace": [
+ {
+ "label": "fuel-price scenario move",
+ "formula": "crude_change_pct × pass_through",
+ "inputs": {
+ "crude_change_pct": 15.0,
+ "pass_through": 0.25
+ },
+ "result": 3.75
+ },
+ {
+ "label": "baseline monthly fuel spend",
+ "formula": "monthly_fuel_liters × fuel_price_per_liter",
+ "inputs": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "result": 1202.5
+ },
+ {
+ "label": "monthly impact",
+ "formula": "baseline_monthly_spend × scenario_fuel_change_pct / 100",
+ "inputs": {
+ "baseline_monthly_spend": 1202.5,
+ "scenario_fuel_change_pct": 3.75
+ },
+ "result": 45.09
+ }
+ ],
+ "details": {
+ "crude_change_pct": 15.0,
+ "scenario_fuel_change_pct": 3.75,
+ "old_monthly_fuel_cost": 1202.5,
+ "scenario_monthly_fuel_cost": 1247.59
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-event.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-event.json
new file mode 100644
index 0000000..9f5e258
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-event.json
@@ -0,0 +1,36 @@
+{
+ "event_id": "demo-jpy-sek-8pct",
+ "event_type": "fx_move",
+ "title": "JPY strengthens sharply against SEK",
+ "source": "synthetic judging fixture",
+ "observed_at": "2026-09-10T16:00:00Z",
+ "old_value": 15.0,
+ "new_value": 13.8,
+ "unit": "JPY per SEK",
+ "change_pct": -7.9999999999999964,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2026-09-03T16:00:00Z",
+ "window_end": "2026-09-10T16:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "metadata": {
+ "demo": true,
+ "z_score": -3.2
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-request.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-request.json
new file mode 100644
index 0000000..1ee0582
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-request.json
@@ -0,0 +1,78 @@
+{
+ "event": {
+ "event_id": "demo-jpy-sek-8pct",
+ "event_type": "fx_move",
+ "title": "JPY strengthens sharply against SEK",
+ "source": "synthetic judging fixture",
+ "observed_at": "2026-09-10T16:00:00Z",
+ "old_value": 15.0,
+ "new_value": 13.8,
+ "unit": "JPY per SEK",
+ "change_pct": -7.9999999999999964,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2026-09-03T16:00:00Z",
+ "window_end": "2026-09-10T16:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "metadata": {
+ "demo": true,
+ "z_score": -3.2
+ }
+ },
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "assumptions": {
+ "interest_rate_pass_through": 0.5,
+ "oil_to_fuel_pass_through": 0.25
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-response.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-response.json
new file mode 100644
index 0000000..203d335
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/fx-response.json
@@ -0,0 +1,94 @@
+{
+ "event_provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "event_observed_at": "2026-09-10T16:00:00Z",
+ "event_id": "demo-jpy-sek-8pct",
+ "headline": "JPY strengthens sharply against SEK",
+ "direct_effect_home": 1739.13,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 8.7,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 21.74,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 38.65,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 12.42,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 1739.13 SEK more than before.",
+ "confidence": "scenario",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [
+ "This event uses a synthetic judging fixture, not a current market observation."
+ ],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0
+ },
+ "result": 300000.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 276000.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 21739.13
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 21739.13,
+ "travel_budget_home": 20000.0
+ },
+ "result": 1739.13
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 300000.0,
+ "new_target_purchasing_power": 276000.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/invalid-request.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/invalid-request.json
new file mode 100644
index 0000000..613f140
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/invalid-request.json
@@ -0,0 +1,6 @@
+{
+ "event": {
+ "event_type": "fx_move"
+ },
+ "profile": {}
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/offline-demo.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/offline-demo.json
new file mode 100644
index 0000000..b611ba6
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/offline-demo.json
@@ -0,0 +1,299 @@
+{
+ "mode": "synthetic_offline_fixture",
+ "profile_source": "Explicit synthetic demonstration inputs; not inferred user finances or local prices.",
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "radar": [
+ {
+ "event": {
+ "event_id": "demo-jpy-sek-8pct",
+ "event_type": "fx_move",
+ "title": "JPY strengthens sharply against SEK",
+ "source": "synthetic judging fixture",
+ "observed_at": "2026-09-10T16:00:00Z",
+ "old_value": 15.0,
+ "new_value": 13.8,
+ "unit": "JPY per SEK",
+ "change_pct": -7.9999999999999964,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2026-09-03T16:00:00Z",
+ "window_end": "2026-09-10T16:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "metadata": {
+ "demo": true,
+ "z_score": -3.2
+ }
+ },
+ "market_significance": 80.0,
+ "user_relevance": 100.0,
+ "priority_score": 91.0,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "FX pair includes the user's home currency",
+ "FX pair includes the user's travel target currency",
+ "User supplied a travel budget"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "demo-brent-15pct",
+ "event_type": "oil_move",
+ "title": "Brent crude rises 15% over the observation window",
+ "source": "demo-fixture",
+ "observed_at": "2026-09-10T16:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": null,
+ "window_end": null,
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "metadata": {
+ "demo": true,
+ "z_score": 2.4
+ }
+ },
+ "market_significance": 60.0,
+ "user_relevance": 100.0,
+ "priority_score": 82.0,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "User supplied monthly fuel use and pump price",
+ "User has direct driving exposure"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "demo-se-policy-rate-25bp",
+ "event_type": "interest_rate_change",
+ "title": "Swedish policy rate rises by 25 basis points",
+ "source": "demo-fixture",
+ "observed_at": "2026-09-10T16:00:00Z",
+ "old_value": 4.0,
+ "new_value": 4.25,
+ "unit": "percent",
+ "change_pct": 6.25,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "SEK"
+ ],
+ "jurisdiction": "SE",
+ "evidence_url": null,
+ "window_start": null,
+ "window_end": null,
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario, not official observations."
+ ]
+ },
+ "metadata": {
+ "demo": true
+ }
+ },
+ "market_significance": 45.0,
+ "user_relevance": 100.0,
+ "priority_score": 75.2,
+ "significance_method": "fallback_policy_rate_basis_points",
+ "relevance_reasons": [
+ "User has declared mortgage exposure",
+ "User has declared savings exposure",
+ "Policy-rate scope matches the user's declared currency exposure",
+ "Policy jurisdiction matches the user's home country"
+ ]
+ }
+ ],
+ "selected_event_id": "demo-jpy-sek-8pct",
+ "impact": {
+ "event_provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "event_observed_at": "2026-09-10T16:00:00Z",
+ "event_id": "demo-jpy-sek-8pct",
+ "headline": "JPY strengthens sharply against SEK",
+ "direct_effect_home": 1739.13,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 8.7,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 21.74,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 38.65,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 12.42,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 1739.13 SEK more than before.",
+ "confidence": "scenario",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [
+ "This event uses a synthetic judging fixture, not a current market observation."
+ ],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0
+ },
+ "result": 300000.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 276000.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 21739.13
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 21739.13,
+ "travel_budget_home": 20000.0
+ },
+ "result": 1739.13
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 300000.0,
+ "new_target_purchasing_power": 276000.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ },
+ "life_conversion": {
+ "amount": 1739.13,
+ "currency": "SEK",
+ "work_hours_equivalent": 8.7,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 21.74,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 38.65,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 12.42,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ]
+ },
+ "boundary": "Only structured old/new observations drive arithmetic; headlines never supply values."
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-event.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-event.json
new file mode 100644
index 0000000..3e99f4f
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-event.json
@@ -0,0 +1,52 @@
+{
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "event_type": "interest_rate_change",
+ "title": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 3.0,
+ "new_value": 3.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": null,
+ "window_start": "2025-12-01T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-01",
+ "old_date": "2025-12-01",
+ "new_date": "2025-12-31",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "change_basis_points": 25.0,
+ "change_percentage_points": 0.25,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-10"
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-request.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-request.json
new file mode 100644
index 0000000..e58b55e
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-request.json
@@ -0,0 +1,94 @@
+{
+ "event": {
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "event_type": "interest_rate_change",
+ "title": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 3.0,
+ "new_value": 3.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": null,
+ "window_start": "2025-12-01T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-01",
+ "old_date": "2025-12-01",
+ "new_date": "2025-12-31",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "change_basis_points": 25.0,
+ "change_percentage_points": 0.25,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-10"
+ }
+ },
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "assumptions": {
+ "interest_rate_pass_through": 0.5,
+ "oil_to_fuel_pass_through": 0.25
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-response.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-response.json
new file mode 100644
index 0000000..cabcd72
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/fixtures/policy-response.json
@@ -0,0 +1,36 @@
+{
+ "event_provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "event_observed_at": "2025-12-31T00:00:00Z",
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "headline": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "direct_effect_home": null,
+ "home_currency": "SEK",
+ "direction": "unquantified",
+ "impact_horizon": null,
+ "work_hours_equivalent": null,
+ "life_units": [],
+ "explanation": "The event is scoped to EUR, while the user's declared rate exposure is SEK; no direct mortgage or savings impact is calculated.",
+ "confidence": "scenario",
+ "assumptions": [],
+ "warnings": [
+ "Cross-border monetary-policy spillovers require a separate model and are outside the MVP.",
+ "Explicit matching affected_currencies are required for a direct rate scenario.",
+ "This event uses a synthetic judging fixture, not a current market observation."
+ ],
+ "calculation_trace": [],
+ "details": {
+ "exposure_currency": "SEK",
+ "event_affected_currencies": [
+ "EUR"
+ ]
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/generate_fixtures.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/generate_fixtures.py
new file mode 100644
index 0000000..ace6cd5
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/generate_fixtures.py
@@ -0,0 +1,45 @@
+"""Regenerate deterministic verification artifacts, without network access."""
+from __future__ import annotations
+
+import asyncio
+import json
+from datetime import datetime, timezone
+from pathlib import Path
+
+from life_exchange_rate.demo import run_demo
+from life_exchange_rate.main import app
+from life_exchange_rate.models import TranslateRequest
+from life_exchange_rate.providers.fixtures import demo_events, demo_profile
+from life_exchange_rate.providers.policy_rate import PolicyRateProvider
+from life_exchange_rate.providers.energy import EnergyProvider
+from life_exchange_rate.service import translate_request
+
+ROOT = Path(__file__).resolve().parent
+FIXED = datetime(2026, 9, 10, 16, tzinfo=timezone.utc)
+
+
+def save(path, value):
+ path.write_text(json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n", encoding="utf-8")
+
+
+async def main():
+ fixtures = ROOT / "fixtures"
+ fixtures.mkdir(exist_ok=True)
+ save(ROOT / "openapi.json", app.openapi())
+ events = {
+ "fx": demo_events()[0],
+ "policy": await PolicyRateProvider(now=lambda: FIXED).policy_rate_move(mode="fixture"),
+ "energy": await EnergyProvider(now=lambda: FIXED).oil_move(mode="fixture"),
+ }
+ for name, event in events.items():
+ request = TranslateRequest(event=event, profile=demo_profile())
+ save(fixtures / f"{name}-event.json", event.model_dump(mode="json"))
+ save(fixtures / f"{name}-request.json", request.model_dump(mode="json"))
+ save(fixtures / f"{name}-response.json", translate_request(request))
+ save(fixtures / "invalid-request.json", {"event": {"event_type": "fx_move"}, "profile": {}})
+ save(fixtures / "offline-demo.json", await run_demo())
+ print("Updated OpenAPI and deterministic event, request, response and demo fixtures.")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/hosted-live-2026-09-16.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/hosted-live-2026-09-16.json
new file mode 100644
index 0000000..e4e2fd9
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/hosted-live-2026-09-16.json
@@ -0,0 +1,704 @@
+{
+ "recorded_at": "2026-09-16T06:40:40.841320+00:00",
+ "origin": "https://life-exchange-rate-mcp.vercel.app",
+ "endpoint": "https://life-exchange-rate-mcp.vercel.app/mcp/",
+ "expected_commit": "c8f2e6f073db92f6d6a75b2a478159044216f7db",
+ "transport": "Public HTTPS Streamable HTTP on Vercel",
+ "live_provider_calls": true,
+ "before": {
+ "health": {
+ "status": "ok",
+ "version": "0.2.0",
+ "commit": "c8f2e6f073db92f6d6a75b2a478159044216f7db",
+ "mcp": "enabled"
+ },
+ "proof": {
+ "schemaVersion": 1,
+ "slug": "kongkoukk-life-exchange-rate",
+ "version": "0.2.0",
+ "commit": "c8f2e6f073db92f6d6a75b2a478159044216f7db",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "health": "https://life-exchange-rate-mcp.vercel.app/health",
+ "openapi": "https://life-exchange-rate-mcp.vercel.app/openapi.json",
+ "mcp": "https://life-exchange-rate-mcp.vercel.app/mcp/"
+ }
+ },
+ "modes": [
+ {
+ "mode": "auto",
+ "protocol_version": "2026-07-28",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ },
+ {
+ "mode": "legacy",
+ "protocol_version": "2025-11-25",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ }
+ ],
+ "end_to_end": {
+ "event_mode": "live_official_observations",
+ "profile_origin": "Explicit synthetic judging profile; prices and income are inputs, not claimed market observations.",
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-15T00:00:00Z",
+ "old_value": 16.0689,
+ "new_value": 15.8648,
+ "unit": "JPY per SEK",
+ "change_pct": -1.2701553933374354,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-08T00:00:00Z",
+ "window_end": "2026-09-15T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T06:40:43.931596Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-08",
+ "new_date": "2026-09-15",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-08",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 1,
+ "query_start_date": "2025-08-18",
+ "query_end_date": "2026-09-16",
+ "history_start_date": "2025-08-18",
+ "pre_query_observation_count": 0,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-29",
+ "reference_window_end": "2026-09-08",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.012782908288157646,
+ "reference_mean_log_return": 0.0005772762267621959,
+ "reference_sample_stddev": 0.011447140325383607,
+ "z_score": -1.1671198338762503,
+ "fallback_reason": null
+ },
+ "z_score": -1.1671198338762503
+ }
+ },
+ "ranked_radar": [
+ {
+ "event": {
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-16T06:40:42.269970Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-16"
+ }
+ },
+ "market_significance": 90.0,
+ "user_relevance": 100.0,
+ "priority_score": 95.5,
+ "significance_method": "fallback_oil_percent_move",
+ "relevance_reasons": [
+ "User supplied monthly fuel use and pump price",
+ "User has direct driving exposure"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-15T00:00:00Z",
+ "old_value": 16.0689,
+ "new_value": 15.8648,
+ "unit": "JPY per SEK",
+ "change_pct": -1.2701553933374354,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-08T00:00:00Z",
+ "window_end": "2026-09-15T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T06:40:43.931596Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-08",
+ "new_date": "2026-09-15",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-08",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 1,
+ "query_start_date": "2025-08-18",
+ "query_end_date": "2026-09-16",
+ "history_start_date": "2025-08-18",
+ "pre_query_observation_count": 0,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-29",
+ "reference_window_end": "2026-09-08",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.012782908288157646,
+ "reference_mean_log_return": 0.0005772762267621959,
+ "reference_sample_stddev": 0.011447140325383607,
+ "z_score": -1.1671198338762503,
+ "fallback_reason": null
+ },
+ "z_score": -1.1671198338762503
+ }
+ },
+ "market_significance": 29.2,
+ "user_relevance": 100.0,
+ "priority_score": 68.1,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "FX pair includes the user's home currency",
+ "FX pair includes the user's travel target currency",
+ "User supplied a travel budget"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "event_type": "interest_rate_change",
+ "title": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 3.0,
+ "new_value": 3.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": null,
+ "window_start": "2025-12-01T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-16T06:40:42.122242Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-01",
+ "old_date": "2025-12-01",
+ "new_date": "2025-12-31",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "change_basis_points": 25.0,
+ "change_percentage_points": 0.25,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-16"
+ }
+ },
+ "market_significance": 45.0,
+ "user_relevance": 0.0,
+ "priority_score": 20.2,
+ "significance_method": "fallback_policy_rate_basis_points",
+ "relevance_reasons": [
+ "Policy-rate currency scope does not support a direct home-currency impact"
+ ]
+ }
+ ],
+ "impact": {
+ "event_provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T06:40:43.931596Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "event_observed_at": "2026-09-15T00:00:00Z",
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "headline": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "direct_effect_home": 257.3,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 1.29,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 3.22,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 5.72,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 1.84,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 257.30 SEK more than before.",
+ "confidence": "observed",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.0689
+ },
+ "result": 321378.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 15.8648
+ },
+ "result": 317296.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.0689,
+ "new_fx_rate": 15.8648
+ },
+ "result": 20257.3
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 20257.3,
+ "travel_budget_home": 20000.0
+ },
+ "result": 257.3
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 321378.0,
+ "new_target_purchasing_power": 317296.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ },
+ "life_unit_conversion": {
+ "amount": 257.3,
+ "currency": "SEK",
+ "work_hours_equivalent": 1.29,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 3.22,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 5.72,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 1.84,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ]
+ },
+ "separately_fetched_official_headline": {
+ "headline_id": "fed-9014ded4c9180ac8",
+ "title": "Minutes of the Board's discount rate meetings on July 20 and July 29, 2026",
+ "publisher": "Federal Reserve Board",
+ "published_at": "2026-08-25T18:00:00Z",
+ "url": "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260825a.htm",
+ "jurisdiction": "US",
+ "event_type_hint": null,
+ "tags": [],
+ "requires_quantification": true,
+ "source_type": "official_rss"
+ },
+ "headline_relationship": "Shown to test the headline boundary; no causal link to the FX move is asserted. Only the numeric FX observations drive this calculation."
+ },
+ "after": {
+ "health": {
+ "status": "ok",
+ "version": "0.2.0",
+ "commit": "c8f2e6f073db92f6d6a75b2a478159044216f7db",
+ "mcp": "enabled"
+ },
+ "proof": {
+ "schemaVersion": 1,
+ "slug": "kongkoukk-life-exchange-rate",
+ "version": "0.2.0",
+ "commit": "c8f2e6f073db92f6d6a75b2a478159044216f7db",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "health": "https://life-exchange-rate-mcp.vercel.app/health",
+ "openapi": "https://life-exchange-rate-mcp.vercel.app/openapi.json",
+ "mcp": "https://life-exchange-rate-mcp.vercel.app/mcp/"
+ }
+ },
+ "status": "passed"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-cli-demo.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-cli-demo.json
new file mode 100644
index 0000000..b4fe48b
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-cli-demo.json
@@ -0,0 +1,308 @@
+{
+ "mode": "live_official_structured_events",
+ "profile_source": "Explicit synthetic demonstration inputs; not inferred user finances or local prices.",
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "radar": [
+ {
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.79% from 2026-09-07 to 2026-09-14",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-14T00:00:00Z",
+ "old_value": 16.1127,
+ "new_value": 15.8248,
+ "unit": "JPY per SEK",
+ "change_pct": -1.7867893028480686,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-07T00:00:00Z",
+ "window_end": "2026-09-14T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-14T15:02:26.602492Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-07 through 2026-09-14.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-07",
+ "new_date": "2026-09-14",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-07",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 0,
+ "query_start_date": "2025-08-16",
+ "query_end_date": "2026-09-14",
+ "history_start_date": "2025-08-15",
+ "pre_query_observation_count": 1,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-28",
+ "reference_window_end": "2026-09-07",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.018029451191580836,
+ "reference_mean_log_return": 0.0007798034164666606,
+ "reference_sample_stddev": 0.011199713290856535,
+ "z_score": -1.6794407249160035,
+ "fallback_reason": null
+ },
+ "z_score": -1.6794407249160035
+ }
+ },
+ "market_significance": 42.0,
+ "user_relevance": 100.0,
+ "priority_score": 73.9,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "FX pair includes the user's home currency",
+ "FX pair includes the user's travel target currency",
+ "User supplied a travel budget"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "ecb-deposit-rate-2026-08-15-2026-09-14",
+ "event_type": "interest_rate_change",
+ "title": "Euro-area deposit facility rate moved +0.0 basis points",
+ "source": "European Central Bank",
+ "observed_at": "2026-09-14T00:00:00Z",
+ "old_value": 2.25,
+ "new_value": 2.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": "https://data.ecb.europa.eu/data/datasets/FM/FM.D.U2.EUR.4F.KR.DFR.LEV",
+ "window_start": "2026-08-15T00:00:00Z",
+ "window_end": "2026-09-14T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "European Central Bank",
+ "source_type": "official_structured",
+ "retrieved_at": "2026-09-14T15:02:27.652854Z",
+ "series_id": "FM.D.U2.EUR.4F.KR.DFR.LEV",
+ "source_url": "https://data-api.ecb.europa.eu/service/data/FM/D.U2.EUR.4F.KR.DFR.LEV",
+ "notes": [
+ "ECB daily deposit facility levels, percent per annum; signed basis-point change is 100 × (new − old).",
+ "Daily levels measure the requested window; this does not identify a specific monetary-policy announcement."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2026-09-14",
+ "target_start_date": "2026-08-15",
+ "old_date": "2026-08-15",
+ "new_date": "2026-09-14",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "ECB",
+ "synthetic": false,
+ "mode": "live",
+ "fallback_used": false,
+ "change_basis_points": 0.0,
+ "change_percentage_points": 0.0,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility"
+ }
+ },
+ "market_significance": 0.0,
+ "user_relevance": 0.0,
+ "priority_score": 0.0,
+ "significance_method": "fallback_policy_rate_basis_points",
+ "relevance_reasons": [
+ "Policy-rate currency scope does not support a direct home-currency impact"
+ ]
+ }
+ ],
+ "selected_event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "impact": {
+ "event_provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-14T15:02:26.602492Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-16&to=2026-09-14&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-07 through 2026-09-14.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "event_observed_at": "2026-09-14T00:00:00Z",
+ "event_id": "fx-SEK-JPY-2026-09-07-2026-09-14",
+ "headline": "SEK/JPY moved -1.79% from 2026-09-07 to 2026-09-14",
+ "direct_effect_home": 363.86,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 1.82,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 4.55,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 8.09,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 2.6,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 363.86 SEK more than before.",
+ "confidence": "observed",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.1127
+ },
+ "result": 322254.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 15.8248
+ },
+ "result": 316496.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.1127,
+ "new_fx_rate": 15.8248
+ },
+ "result": 20363.86
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 20363.86,
+ "travel_budget_home": 20000.0
+ },
+ "result": 363.86
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 322254.0,
+ "new_target_purchasing_power": 316496.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ },
+ "life_conversion": {
+ "amount": 363.86,
+ "currency": "SEK",
+ "work_hours_equivalent": 1.82,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 4.55,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 8.09,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 2.6,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ]
+ },
+ "boundary": "Only structured old/new observations drive arithmetic; headlines never supply values."
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-providers.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-providers.json
new file mode 100644
index 0000000..4cabb6c
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/live-providers.json
@@ -0,0 +1,106 @@
+{
+ "recorded_at": "2026-09-14T09:04:06.912326+00:00",
+ "policy_status": "live_passed",
+ "policy_event": {
+ "event_id": "ecb-deposit-rate-2026-08-15-2026-09-14",
+ "event_type": "interest_rate_change",
+ "title": "Euro-area deposit facility rate moved +0.0 basis points",
+ "source": "European Central Bank",
+ "observed_at": "2026-09-14T00:00:00Z",
+ "old_value": 2.25,
+ "new_value": 2.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": "https://data.ecb.europa.eu/data/datasets/FM/FM.D.U2.EUR.4F.KR.DFR.LEV",
+ "window_start": "2026-08-15T00:00:00Z",
+ "window_end": "2026-09-14T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "European Central Bank",
+ "source_type": "official_structured",
+ "retrieved_at": "2026-09-14T09:04:06.911326Z",
+ "series_id": "FM.D.U2.EUR.4F.KR.DFR.LEV",
+ "source_url": "https://data-api.ecb.europa.eu/service/data/FM/D.U2.EUR.4F.KR.DFR.LEV",
+ "notes": [
+ "ECB daily deposit facility levels, percent per annum; signed basis-point change is 100 \u00d7 (new \u2212 old).",
+ "Daily levels measure the requested window; this does not identify a specific monetary-policy announcement."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2026-09-14",
+ "target_start_date": "2026-08-15",
+ "old_date": "2026-08-15",
+ "new_date": "2026-09-14",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "ECB",
+ "synthetic": false,
+ "mode": "live",
+ "fallback_used": false,
+ "change_basis_points": 0.0,
+ "change_percentage_points": 0.0,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility"
+ }
+ },
+ "energy_status": "fixture_fallback",
+ "energy_event": {
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-14T09:04:06.912326Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "live_or_fixture",
+ "fallback_used": true,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-14",
+ "fallback_reason": "EIA_API_KEY is not configured"
+ }
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/mcp-smoke.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/mcp-smoke.json
new file mode 100644
index 0000000..af61763
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/mcp-smoke.json
@@ -0,0 +1,233 @@
+{
+ "recorded_at": "2026-09-14T15:02:21.201319+00:00",
+ "sdk_version": "2.2.0",
+ "transport": "Streamable HTTP over a real Uvicorn subprocess on loopback",
+ "endpoint": "http://127.0.0.1:53644/mcp/",
+ "live_positive_calls": true,
+ "health": {
+ "status": "ok",
+ "version": "0.2.0",
+ "commit": "dev-local",
+ "mcp": "enabled"
+ },
+ "verification": {
+ "schemaVersion": 1,
+ "slug": "life-exchange-rate",
+ "version": "0.2.0",
+ "commit": "dev-local",
+ "apiBaseUrl": "http://127.0.0.1:53644",
+ "health": "http://127.0.0.1:53644/health",
+ "openapi": "http://127.0.0.1:53644/openapi.json",
+ "mcp": "http://127.0.0.1:53644/mcp/"
+ },
+ "modes": [
+ {
+ "mode": "auto",
+ "protocol_version": "2026-07-28",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ },
+ {
+ "mode": "legacy",
+ "protocol_version": "2025-11-25",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ }
+ ],
+ "status": "passed",
+ "server_stopped": true
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/openapi.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/openapi.json
new file mode 100644
index 0000000..fe75b81
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/openapi.json
@@ -0,0 +1,1412 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Life Exchange Rate API",
+ "description": "Translate macroeconomic shocks into concrete, user-specific life impacts.",
+ "version": "0.2.0"
+ },
+ "paths": {
+ "/health": {
+ "get": {
+ "summary": "Health",
+ "operationId": "health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Response Health Health Get"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/.well-known/xagent-verification.json": {
+ "get": {
+ "summary": "Xagent Verification",
+ "operationId": "xagent_verification__well_known_xagent_verification_json_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Response Xagent Verification Well Known Xagent Verification Json Get"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/events/demo": {
+ "get": {
+ "summary": "Events Demo",
+ "operationId": "events_demo_v1_events_demo_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/MacroEvent"
+ },
+ "type": "array",
+ "title": "Response Events Demo V1 Events Demo Get"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/radar/demo": {
+ "get": {
+ "summary": "Radar Demo",
+ "operationId": "radar_demo_v1_radar_demo_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/EventScore"
+ },
+ "type": "array",
+ "title": "Response Radar Demo V1 Radar Demo Get"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/events/fx": {
+ "get": {
+ "summary": "Events Fx",
+ "operationId": "events_fx_v1_events_fx_get",
+ "parameters": [
+ {
+ "name": "base",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[A-Za-z]{3}$",
+ "title": "Base"
+ }
+ },
+ {
+ "name": "quote",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[A-Za-z]{3}$",
+ "title": "Quote"
+ }
+ },
+ {
+ "name": "lookback_days",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 90,
+ "minimum": 2,
+ "default": 7,
+ "title": "Lookback Days"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MacroEvent"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/events/policy-rate": {
+ "get": {
+ "summary": "Events Policy Rate",
+ "operationId": "events_policy_rate_v1_events_policy_rate_get",
+ "parameters": [
+ {
+ "name": "lookback_days",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 365,
+ "minimum": 2,
+ "default": 30,
+ "title": "Lookback Days"
+ }
+ },
+ {
+ "name": "mode",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "enum": [
+ "live",
+ "fixture",
+ "live_or_fixture"
+ ],
+ "type": "string",
+ "default": "live_or_fixture",
+ "title": "Mode"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MacroEvent"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/events/energy": {
+ "get": {
+ "summary": "Events Energy",
+ "operationId": "events_energy_v1_events_energy_get",
+ "parameters": [
+ {
+ "name": "lookback_days",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 365,
+ "minimum": 2,
+ "default": 7,
+ "title": "Lookback Days"
+ }
+ },
+ {
+ "name": "mode",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "enum": [
+ "live",
+ "fixture",
+ "live_or_fixture"
+ ],
+ "type": "string",
+ "default": "live_or_fixture",
+ "title": "Mode"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MacroEvent"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/headlines/official": {
+ "get": {
+ "summary": "Headlines Official",
+ "operationId": "headlines_official_v1_headlines_official_get",
+ "parameters": [
+ {
+ "name": "source",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "enum": [
+ "fed",
+ "ecb"
+ ],
+ "type": "string",
+ "title": "Source"
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 50,
+ "minimum": 1,
+ "default": 10,
+ "title": "Limit"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MacroHeadline"
+ },
+ "title": "Response Headlines Official V1 Headlines Official Get"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/radar/rank": {
+ "post": {
+ "summary": "Radar Rank",
+ "operationId": "radar_rank_v1_radar_rank_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RadarRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/EventScore"
+ },
+ "type": "array",
+ "title": "Response Radar Rank V1 Radar Rank Post"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/translate": {
+ "post": {
+ "summary": "Translate",
+ "operationId": "translate_v1_translate_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TranslateRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ImpactResult"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/convert": {
+ "post": {
+ "summary": "Convert",
+ "operationId": "convert_v1_convert_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ConvertRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Response Convert V1 Convert Post"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/scenarios": {
+ "post": {
+ "summary": "Scenarios",
+ "operationId": "scenarios_v1_scenarios_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ScenarioRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array",
+ "title": "Response Scenarios V1 Scenarios Post"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "CalculationStep": {
+ "properties": {
+ "label": {
+ "type": "string",
+ "title": "Label"
+ },
+ "formula": {
+ "type": "string",
+ "title": "Formula"
+ },
+ "inputs": {
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "type": "object",
+ "title": "Inputs"
+ },
+ "result": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Result"
+ }
+ },
+ "type": "object",
+ "required": [
+ "label",
+ "formula"
+ ],
+ "title": "CalculationStep"
+ },
+ "CommuteProfile": {
+ "properties": {
+ "monthly_fuel_liters": {
+ "type": "number",
+ "minimum": 0.0,
+ "title": "Monthly Fuel Liters"
+ },
+ "fuel_price_per_liter": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Fuel Price Per Liter"
+ }
+ },
+ "type": "object",
+ "required": [
+ "monthly_fuel_liters",
+ "fuel_price_per_liter"
+ ],
+ "title": "CommuteProfile"
+ },
+ "Confidence": {
+ "type": "string",
+ "enum": [
+ "observed",
+ "scenario",
+ "mixed"
+ ],
+ "title": "Confidence"
+ },
+ "ConvertRequest": {
+ "properties": {
+ "amount": {
+ "type": "number",
+ "title": "Amount"
+ },
+ "currency": {
+ "type": "string",
+ "title": "Currency"
+ },
+ "profile": {
+ "$ref": "#/components/schemas/LifeProfile"
+ }
+ },
+ "type": "object",
+ "required": [
+ "amount",
+ "currency",
+ "profile"
+ ],
+ "title": "ConvertRequest"
+ },
+ "EventScore": {
+ "properties": {
+ "event": {
+ "$ref": "#/components/schemas/MacroEvent"
+ },
+ "market_significance": {
+ "type": "number",
+ "maximum": 100.0,
+ "minimum": 0.0,
+ "title": "Market Significance"
+ },
+ "user_relevance": {
+ "type": "number",
+ "maximum": 100.0,
+ "minimum": 0.0,
+ "title": "User Relevance"
+ },
+ "priority_score": {
+ "type": "number",
+ "maximum": 100.0,
+ "minimum": 0.0,
+ "title": "Priority Score"
+ },
+ "significance_method": {
+ "type": "string",
+ "title": "Significance Method"
+ },
+ "relevance_reasons": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Relevance Reasons"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event",
+ "market_significance",
+ "user_relevance",
+ "priority_score",
+ "significance_method"
+ ],
+ "title": "EventScore"
+ },
+ "EventType": {
+ "type": "string",
+ "enum": [
+ "fx_move",
+ "interest_rate_change",
+ "oil_move"
+ ],
+ "title": "EventType"
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "ImpactAssumptions": {
+ "properties": {
+ "interest_rate_pass_through": {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "title": "Interest Rate Pass Through",
+ "default": 0.5
+ },
+ "oil_to_fuel_pass_through": {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "title": "Oil To Fuel Pass Through",
+ "default": 0.25
+ }
+ },
+ "type": "object",
+ "title": "ImpactAssumptions"
+ },
+ "ImpactDirection": {
+ "type": "string",
+ "enum": [
+ "cost_increase",
+ "cost_decrease",
+ "neutral",
+ "unquantified"
+ ],
+ "title": "ImpactDirection"
+ },
+ "ImpactResult": {
+ "properties": {
+ "event_provenance": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Provenance"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event_observed_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Event Observed At"
+ },
+ "event_id": {
+ "type": "string",
+ "title": "Event Id"
+ },
+ "headline": {
+ "type": "string",
+ "title": "Headline"
+ },
+ "direct_effect_home": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Direct Effect Home"
+ },
+ "home_currency": {
+ "type": "string",
+ "title": "Home Currency"
+ },
+ "direction": {
+ "$ref": "#/components/schemas/ImpactDirection",
+ "default": "unquantified"
+ },
+ "impact_horizon": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Impact Horizon"
+ },
+ "work_hours_equivalent": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Work Hours Equivalent"
+ },
+ "life_units": {
+ "items": {
+ "$ref": "#/components/schemas/LifeUnitConversion"
+ },
+ "type": "array",
+ "title": "Life Units"
+ },
+ "explanation": {
+ "type": "string",
+ "title": "Explanation"
+ },
+ "confidence": {
+ "$ref": "#/components/schemas/Confidence"
+ },
+ "assumptions": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Assumptions"
+ },
+ "warnings": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Warnings"
+ },
+ "calculation_trace": {
+ "items": {
+ "$ref": "#/components/schemas/CalculationStep"
+ },
+ "type": "array",
+ "title": "Calculation Trace"
+ },
+ "details": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Details"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event_id",
+ "headline",
+ "home_currency",
+ "explanation",
+ "confidence"
+ ],
+ "title": "ImpactResult"
+ },
+ "LifeProfile": {
+ "properties": {
+ "home_currency": {
+ "type": "string",
+ "title": "Home Currency"
+ },
+ "home_country": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Home Country"
+ },
+ "net_monthly_income": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Net Monthly Income"
+ },
+ "monthly_work_hours": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Monthly Work Hours"
+ },
+ "travel_budget_home": {
+ "anyOf": [
+ {
+ "type": "number",
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Travel Budget Home"
+ },
+ "travel_target_currency": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Travel Target Currency"
+ },
+ "mortgage": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MortgageProfile"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "savings_balance": {
+ "anyOf": [
+ {
+ "type": "number",
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Savings Balance"
+ },
+ "commute": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/CommuteProfile"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "life_units": {
+ "items": {
+ "$ref": "#/components/schemas/LifeUnit"
+ },
+ "type": "array",
+ "title": "Life Units"
+ }
+ },
+ "type": "object",
+ "required": [
+ "home_currency",
+ "net_monthly_income",
+ "monthly_work_hours"
+ ],
+ "title": "LifeProfile"
+ },
+ "LifeUnit": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "price": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Price"
+ },
+ "currency": {
+ "type": "string",
+ "title": "Currency"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "price",
+ "currency"
+ ],
+ "title": "LifeUnit"
+ },
+ "LifeUnitConversion": {
+ "properties": {
+ "unit": {
+ "type": "string",
+ "title": "Unit"
+ },
+ "quantity": {
+ "type": "number",
+ "title": "Quantity"
+ },
+ "currency": {
+ "type": "string",
+ "title": "Currency"
+ },
+ "unit_price": {
+ "type": "number",
+ "title": "Unit Price"
+ }
+ },
+ "type": "object",
+ "required": [
+ "unit",
+ "quantity",
+ "currency",
+ "unit_price"
+ ],
+ "title": "LifeUnitConversion"
+ },
+ "MacroEvent": {
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "title": "Event Id"
+ },
+ "event_type": {
+ "$ref": "#/components/schemas/EventType"
+ },
+ "title": {
+ "type": "string",
+ "title": "Title"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source"
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Observed At"
+ },
+ "old_value": {
+ "type": "number",
+ "title": "Old Value"
+ },
+ "new_value": {
+ "type": "number",
+ "title": "New Value"
+ },
+ "unit": {
+ "type": "string",
+ "title": "Unit"
+ },
+ "change_pct": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Change Pct"
+ },
+ "base_currency": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Base Currency"
+ },
+ "quote_currency": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Quote Currency"
+ },
+ "affected_currencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Currencies"
+ },
+ "jurisdiction": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Jurisdiction"
+ },
+ "evidence_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Evidence Url"
+ },
+ "window_start": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Window Start"
+ },
+ "window_end": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Window End"
+ },
+ "confidence": {
+ "$ref": "#/components/schemas/Confidence",
+ "default": "observed"
+ },
+ "provenance": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Provenance"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Metadata"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event_id",
+ "event_type",
+ "title",
+ "source",
+ "old_value",
+ "new_value",
+ "unit"
+ ],
+ "title": "MacroEvent"
+ },
+ "MacroHeadline": {
+ "properties": {
+ "headline_id": {
+ "type": "string",
+ "title": "Headline Id"
+ },
+ "title": {
+ "type": "string",
+ "title": "Title"
+ },
+ "publisher": {
+ "type": "string",
+ "title": "Publisher"
+ },
+ "published_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Published At"
+ },
+ "url": {
+ "type": "string",
+ "title": "Url"
+ },
+ "jurisdiction": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Jurisdiction"
+ },
+ "event_type_hint": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "tags": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Tags"
+ },
+ "requires_quantification": {
+ "type": "boolean",
+ "title": "Requires Quantification",
+ "default": true
+ },
+ "source_type": {
+ "type": "string",
+ "title": "Source Type",
+ "default": "official_rss"
+ }
+ },
+ "type": "object",
+ "required": [
+ "headline_id",
+ "title",
+ "publisher",
+ "url"
+ ],
+ "title": "MacroHeadline"
+ },
+ "MortgageProfile": {
+ "properties": {
+ "principal": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Principal"
+ },
+ "remaining_years": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Remaining Years"
+ },
+ "current_annual_rate_pct": {
+ "type": "number",
+ "maximum": 100.0,
+ "minimum": 0.0,
+ "title": "Current Annual Rate Pct"
+ },
+ "currency": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Currency"
+ }
+ },
+ "type": "object",
+ "required": [
+ "principal",
+ "remaining_years",
+ "current_annual_rate_pct"
+ ],
+ "title": "MortgageProfile"
+ },
+ "Provenance": {
+ "properties": {
+ "provider": {
+ "type": "string",
+ "title": "Provider"
+ },
+ "source_type": {
+ "type": "string",
+ "title": "Source Type",
+ "default": "official_or_structured"
+ },
+ "retrieved_at": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Retrieved At"
+ },
+ "series_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Series Id"
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url"
+ },
+ "notes": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Notes"
+ }
+ },
+ "type": "object",
+ "required": [
+ "provider"
+ ],
+ "title": "Provenance"
+ },
+ "RadarRequest": {
+ "properties": {
+ "events": {
+ "items": {
+ "$ref": "#/components/schemas/MacroEvent"
+ },
+ "type": "array",
+ "title": "Events"
+ },
+ "profile": {
+ "$ref": "#/components/schemas/LifeProfile"
+ }
+ },
+ "type": "object",
+ "required": [
+ "events",
+ "profile"
+ ],
+ "title": "RadarRequest"
+ },
+ "ScenarioInput": {
+ "properties": {
+ "label": {
+ "type": "string",
+ "title": "Label"
+ },
+ "multiplier": {
+ "type": "number",
+ "exclusiveMinimum": 0.0,
+ "title": "Multiplier"
+ }
+ },
+ "type": "object",
+ "required": [
+ "label",
+ "multiplier"
+ ],
+ "title": "ScenarioInput"
+ },
+ "ScenarioRequest": {
+ "properties": {
+ "event": {
+ "$ref": "#/components/schemas/MacroEvent"
+ },
+ "profile": {
+ "$ref": "#/components/schemas/LifeProfile"
+ },
+ "scenarios": {
+ "items": {
+ "$ref": "#/components/schemas/ScenarioInput"
+ },
+ "type": "array",
+ "title": "Scenarios"
+ },
+ "assumptions": {
+ "$ref": "#/components/schemas/ImpactAssumptions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event",
+ "profile",
+ "scenarios"
+ ],
+ "title": "ScenarioRequest"
+ },
+ "TranslateRequest": {
+ "properties": {
+ "event": {
+ "$ref": "#/components/schemas/MacroEvent"
+ },
+ "profile": {
+ "$ref": "#/components/schemas/LifeProfile"
+ },
+ "assumptions": {
+ "$ref": "#/components/schemas/ImpactAssumptions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event",
+ "profile"
+ ],
+ "title": "TranslateRequest"
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ },
+ "input": {
+ "title": "Input"
+ },
+ "ctx": {
+ "type": "object",
+ "title": "Context"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ }
+ }
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/record_live_providers.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/record_live_providers.py
new file mode 100644
index 0000000..ed05cfd
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/record_live_providers.py
@@ -0,0 +1,26 @@
+"""Record actual provider checks; never treat missing-key fallback as live EIA."""
+import asyncio
+import json
+from datetime import datetime, timezone
+from pathlib import Path
+
+from life_exchange_rate.providers.policy_rate import PolicyRateProvider
+from life_exchange_rate.providers.energy import EnergyProvider
+
+
+async def main():
+ policy = await PolicyRateProvider().policy_rate_move(mode="live")
+ energy = await EnergyProvider().oil_move(mode="live_or_fixture")
+ record = {
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
+ "policy_status": "live_passed",
+ "policy_event": policy.model_dump(mode="json"),
+ "energy_status": "fixture_fallback" if energy.metadata.get("synthetic") else "live_passed",
+ "energy_event": energy.model_dump(mode="json"),
+ }
+ (Path(__file__).parent / "live-providers.json").write_text(json.dumps(record, indent=2, allow_nan=False) + "\n", encoding="utf-8")
+ print(json.dumps({k: v for k, v in record.items() if not k.endswith("_event")}))
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/smoke_mcp.py b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/smoke_mcp.py
new file mode 100644
index 0000000..5a54863
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/smoke_mcp.py
@@ -0,0 +1,227 @@
+"""Exercise the deployed ASGI mount using the real MCP v2 HTTP client.
+
+Run from the repository root: uv run python verification/smoke_mcp.py --live-fx
+Without --live-fx, the deterministic chain is offline and positive FX/RSS calls
+are explicitly reported as skipped. The server is stopped on every exit path.
+"""
+from __future__ import annotations
+
+import argparse
+import asyncio
+import json
+import os
+from pathlib import Path
+import re
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+import traceback
+from datetime import datetime, timezone
+from importlib.metadata import version
+
+import httpx
+from mcp import Client
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(PROJECT_ROOT / "source"))
+
+from life_exchange_rate.providers.fixtures import demo_profile
+
+EXPECTED_TOOLS = {
+ "get_macro_events", "get_official_macro_headlines", "get_live_fx_event",
+ "get_policy_rate_event", "get_energy_event", "rank_events_for_user",
+ "translate_event_to_life", "convert_to_life_units", "compare_event_scenarios",
+}
+
+
+def diagnostic(exc: BaseException) -> str:
+ detail = "".join(traceback.format_exception(exc))
+ return re.sub(r"(?i)(api[_-]?key|access[_-]?token|password)=([^&\s]+)", r"\1=[redacted]", detail)
+
+
+def unpack(result):
+ if result.is_error:
+ raise AssertionError(f"MCP tool returned an error: {result.content}")
+ if result.structured_content is None:
+ raise AssertionError("Expected structured MCP output")
+ value = result.structured_content
+ return value["result"] if set(value) == {"result"} else value
+
+
+async def exercise(endpoint: str, live_fx: bool):
+ profile = demo_profile().model_dump(mode="json")
+ modes = []
+ evidence = None
+ for mode, expected_protocol in (("auto", "2026-07-28"), ("legacy", "2025-11-25")):
+ calls = []
+ negative_checks = []
+ async with Client(endpoint, mode=mode, read_timeout_seconds=45) as client:
+ listed = await client.list_tools()
+ names = {tool.name for tool in listed.tools}
+ assert names == EXPECTED_TOOLS, names
+ assert client.protocol_version == expected_protocol
+
+ async def call(name, arguments):
+ print(f"MCP smoke [{mode}]: {name}", flush=True)
+ result = unpack(await client.call_tool(name, arguments))
+ calls.append({"tool": name, "status": "passed"})
+ return result
+
+ events = await call("get_macro_events", {"mode": "demo"})
+ policy = await call("get_policy_rate_event", {"mode": "fixture"})
+ energy = await call("get_energy_event", {"mode": "fixture"})
+ assert policy["provenance"]["source_type"] == "synthetic_fixture"
+ assert energy["provenance"]["source_type"] == "synthetic_fixture"
+ fixture_radar = await call("rank_events_for_user", {"events": events, "profile": profile})
+ fixture_impact = await call("translate_event_to_life", {"event": events[0], "profile": profile})
+ assert fixture_impact["direct_effect_home"] == 1739.13
+ assert fixture_impact["work_hours_equivalent"] == 8.7
+ assert fixture_impact["calculation_trace"]
+ conversion = await call("convert_to_life_units", {
+ "amount": fixture_impact["direct_effect_home"], "currency": "SEK", "profile": profile,
+ })
+ assert conversion["work_hours_equivalent"] == 8.7
+ scenarios = await call("compare_event_scenarios", {
+ "event": events[2], "profile": profile,
+ "assumptions": {"interest_rate_pass_through": 0, "oil_to_fuel_pass_through": 0},
+ })
+ assert len(scenarios) == 3
+ assert all(item["result"]["direct_effect_home"] == 0 for item in scenarios)
+
+ headline = {"headline_id": "unquantified-smoke-input", "title": "A headline without observations", "requires_quantification": True}
+ current_evidence = {
+ "event_mode": "synthetic_fixture",
+ "profile_origin": "Explicit synthetic judging profile; prices and income are inputs, not claimed market observations.",
+ "profile": profile,
+ "event": events[0],
+ "ranked_radar": fixture_radar,
+ "impact": fixture_impact,
+ "life_unit_conversion": conversion,
+ }
+ if live_fx:
+ headlines = await call("get_official_macro_headlines", {"source": "fed", "limit": 1})
+ assert headlines and all(item["requires_quantification"] for item in headlines)
+ headline = headlines[0]
+ observed = await call("get_live_fx_event", {"base_currency": "SEK", "quote_currency": "JPY", "lookback_days": 7})
+ assert observed["confidence"] == "observed"
+ assert observed["provenance"]["source_url"]
+ assert observed["window_start"] <= observed["window_end"]
+ radar = await call("rank_events_for_user", {"events": [observed, policy, energy], "profile": profile})
+ impact = await call("translate_event_to_life", {"event": observed, "profile": profile})
+ expected_money = round(profile["travel_budget_home"] * (observed["old_value"] / observed["new_value"] - 1), 2)
+ assert impact["direct_effect_home"] == expected_money
+ assert impact["calculation_trace"]
+ assert impact["event_provenance"] == observed["provenance"]
+ units = await call("convert_to_life_units", {"amount": impact["direct_effect_home"], "currency": "SEK", "profile": profile})
+ current_evidence.update({
+ "event_mode": "live_official_observations",
+ "separately_fetched_official_headline": headline,
+ "headline_relationship": "Shown to test the headline boundary; no causal link to the FX move is asserted. Only the numeric FX observations drive this calculation.",
+ "event": observed, "ranked_radar": radar, "impact": impact, "life_unit_conversion": units,
+ })
+ else:
+ for name in ("get_official_macro_headlines", "get_live_fx_event"):
+ calls.append({"tool": name, "status": "skipped", "reason": "Pass --live-fx to exercise live positive calls."})
+
+ invalid = [
+ ("get_macro_events", {"mode": "invent"}, "unsupported event mode"),
+ ("get_official_macro_headlines", {"source": "untrusted"}, "unknown headline source"),
+ ("get_live_fx_event", {"base_currency": "SEK", "quote_currency": "SEK"}, "identical FX currencies"),
+ ("get_energy_event", {"lookback_days": 366}, "out-of-range lookback"),
+ ("translate_event_to_life", {"event": headline, "profile": profile}, "headline lacks numeric event values"),
+ ("convert_to_life_units", {"amount": 100, "currency": "USD", "profile": profile}, "conversion currency mismatch"),
+ ("compare_event_scenarios", {"event": events[0], "profile": profile, "mild_multiplier": 0}, "invalid scenario multiplier"),
+ ]
+ for name, arguments, label in invalid:
+ result = await client.call_tool(name, arguments)
+ assert result.is_error, label
+ negative_checks.append({"check": label, "status": "rejected_as_expected"})
+ modes.append({
+ "mode": mode,
+ "protocol_version": client.protocol_version,
+ "server_info": client.server_info.model_dump(mode="json") if client.server_info else None,
+ "tool_names": sorted(names),
+ "calls": calls,
+ "negative_checks": negative_checks,
+ })
+ if evidence is None:
+ evidence = current_evidence
+ return modes, evidence
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--live-fx", action="store_true", help="Exercise live official RSS and ECB-filtered FX and save the observed end-to-end chain")
+ parser.add_argument("--output-dir", type=Path, default=PROJECT_ROOT / "verification")
+ args = parser.parse_args()
+ with socket.socket() as port_socket:
+ port_socket.bind(("127.0.0.1", 0))
+ port = port_socket.getsockname()[1]
+ base_url = f"http://127.0.0.1:{port}"
+ environment = os.environ.copy()
+ environment.update({"PUBLIC_BASE_URL": base_url, "PYTHONDONTWRITEBYTECODE": "1"})
+ output = {
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
+ "sdk_version": version("mcp"),
+ "transport": "Streamable HTTP over a real Uvicorn subprocess on loopback",
+ "endpoint": base_url + "/mcp/",
+ "live_positive_calls": args.live_fx,
+ }
+ evidence = None
+ failure = None
+ args.output_dir.mkdir(parents=True, exist_ok=True)
+ with tempfile.TemporaryFile(mode="w+", encoding="utf-8") as server_log:
+ process = subprocess.Popen(
+ [sys.executable, "-m", "uvicorn", "life_exchange_rate.main:app", "--app-dir", "source", "--host", "127.0.0.1", "--port", str(port), "--no-access-log"],
+ cwd=PROJECT_ROOT, env=environment, stdout=server_log, stderr=subprocess.STDOUT,
+ creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
+ )
+ try:
+ with httpx.Client(base_url=base_url, timeout=2, trust_env=False) as http:
+ for _ in range(150):
+ if process.poll() is not None:
+ raise RuntimeError("Uvicorn exited before startup completed")
+ try:
+ health = http.get("/health")
+ if health.status_code == 200:
+ break
+ except httpx.HTTPError:
+ pass
+ time.sleep(0.1)
+ else:
+ raise RuntimeError("Uvicorn startup timed out")
+ proof = http.get("/.well-known/xagent-verification.json").json()
+ assert health.json()["commit"] == proof["commit"]
+ assert health.json()["version"] == proof["version"]
+ assert proof["mcp"] == base_url + "/mcp/"
+ output.update({"health": health.json(), "verification": proof})
+ output["modes"], evidence = asyncio.run(exercise(base_url + "/mcp/", args.live_fx))
+ output["status"] = "passed" if args.live_fx else "passed_fixture_flow_live_calls_skipped"
+ except Exception as exc:
+ details = diagnostic(exc)
+ output.update({"status": "failed", "error": str(exc), "error_traceback": details})
+ print(details, file=sys.stderr, flush=True)
+ failure = exc
+ finally:
+ process.terminate()
+ try:
+ process.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ process.kill()
+ process.wait(timeout=5)
+ output["server_stopped"] = process.poll() is not None
+ (args.output_dir / "mcp-smoke.json").write_text(json.dumps(output, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
+ if evidence is not None:
+ evidence.update({"recorded_at": output["recorded_at"], "review_commit": output["health"]["commit"]})
+ else:
+ evidence = {"status": "not_generated", "recorded_at": output["recorded_at"], "reason": "The smoke run failed before completing the end-to-end chain; no live result is claimed."}
+ (args.output_dir / "end-to-end-demo.json").write_text(json.dumps(evidence, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
+ print(json.dumps({"status": output["status"], "sdk_version": output["sdk_version"], "output_dir": str(args.output_dir.resolve())}))
+ if failure is not None:
+ raise SystemExit(f"MCP smoke failed: {failure}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/summary.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/summary.json
new file mode 100644
index 0000000..18a4db8
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/source/verification/summary.json
@@ -0,0 +1,34 @@
+{
+ "status": "passed",
+ "tests": 145,
+ "mcp_smoke": "passed",
+ "mcp_protocols": [
+ "2026-07-28",
+ "2025-11-25"
+ ],
+ "mcp_tools": 9,
+ "review_commit": "dev-local",
+ "versions": {
+ "mcp": "2.2.0",
+ "pydantic": "2.13.5",
+ "fastapi": "0.141.1",
+ "uvicorn": "0.53.0",
+ "pytest": "9.1.1"
+ },
+ "wheel_check": {
+ "status": "passed",
+ "wheel": "life_exchange_rate_mcp-0.2.0-py3-none-any.whl",
+ "sha256": "3a4b358a61583d5af31d6d6fd7e21167da069f130f9076b1e9d1248a7ad35664",
+ "fixture_paths": [
+ "life_exchange_rate/providers/data/policy_rate.json",
+ "life_exchange_rate/providers/data/energy.json"
+ ],
+ "packaged_fixture_execution": "passed for both providers",
+ "docker_build": "not_run_docker_unavailable"
+ },
+ "pending_external_submission": [
+ "Actual source repository and reviewed Git SHA",
+ "Public deployment URL",
+ "Optional keyed EIA live test"
+ ]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/submission.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/submission.json
new file mode 100644
index 0000000..3f6c419
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/submission.json
@@ -0,0 +1,10 @@
+{
+ "schemaVersion": 1,
+ "name": "Life Exchange Rate MCP",
+ "slug": "kongkoukk-life-exchange-rate",
+ "sourceRepository": "https://github.com/KongkouKK/life-exchange-rate-mcp",
+ "reviewCommit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "healthCheckUrl": "https://life-exchange-rate-mcp.vercel.app/health",
+ "deploymentProofUrl": "https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/README.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/README.md
new file mode 100644
index 0000000..30dd90a
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/README.md
@@ -0,0 +1,77 @@
+# Verification evidence
+
+## Prerequisites
+
+- **Public source:** https://github.com/KongkouKK/life-exchange-rate-mcp
+- **Review commit:** `238954196ea6922d3cea2ae8acb914b0f4e01dbb`
+- **API base URL:** https://life-exchange-rate-mcp.vercel.app
+- **Authentication:** None; no review credential is needed.
+- Capability fixture commands below run from the complete runnable project at `source/` in the submission root. If starting in this `verification/` directory, enter `../source` first.
+
+## 1. Health check
+
+```bash
+curl --fail --silent --show-error --max-time 60 https://life-exchange-rate-mcp.vercel.app/health
+```
+
+Expected response:
+
+```json
+{"status":"ok","version":"0.2.0","commit":"238954196ea6922d3cea2ae8acb914b0f4e01dbb","mcp":"enabled"}
+```
+
+## 2. Deployment proof
+
+```bash
+curl --fail --silent --show-error --max-time 60 https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json
+```
+
+Required fields in the response:
+
+```json
+{"schemaVersion":1,"slug":"kongkoukk-life-exchange-rate","commit":"238954196ea6922d3cea2ae8acb914b0f4e01dbb"}
+```
+
+The full response also reports version and production-origin health/OpenAPI/MCP URLs. Check these refer to `https://life-exchange-rate-mcp.vercel.app` and retain the canonical `/mcp/` trailing slash. Do not add a redirect-following option.
+
+## 3. Capability call and safe failure
+
+Run from the runnable project directory:
+
+```bash
+curl --fail --silent --show-error --max-time 60 \
+ https://life-exchange-rate-mcp.vercel.app/v1/translate \
+ --header 'Content-Type: application/json' \
+ --data-binary @verification/fixtures/fx-request.json
+
+curl --silent --show-error --max-time 60 --write-out '\nHTTP %{http_code}\n' \
+ https://life-exchange-rate-mcp.vercel.app/v1/translate \
+ --header 'Content-Type: application/json' \
+ --data-binary @verification/fixtures/invalid-request.json
+```
+
+The valid fixture expects HTTP 200 with `direct_effect_home: 1739.13`, `home_currency: SEK`, `work_hours_equivalent: 8.7`, calculation trace and synthetic event provenance. The invalid fixture expects HTTP 422 with validation details. Use `curl.exe` on Windows PowerShell when necessary. The fixture contains synthetic household values.
+
+The [public REST record](hosted-rest-verification-final.json), captured at 2026-09-16 10:00:54 UTC, confirms those results using published fixture files and the same review commit before and after the calls. Provider failures in strict live mode return sanitized HTTP 502 errors. A policy/energy fallback is labeled as synthetic and is not a live-provider success.
+
+## 4. MCP and live-provider checks
+
+Connect a real MCP client to `https://life-exchange-rate-mcp.vercel.app/mcp/`; a browser GET does not exercise tools.
+
+The [complete hosted record](hosted-live-verification-final.json), captured at 2026-09-16 09:55:43 UTC, passed all nine tools in current protocol `2026-07-28` and legacy protocol `2025-11-25`. Each mode passed 12 positive calls and rejected 7 invalid-input cases. FX and Fed RSS were live. Policy and energy were labeled synthetic fixtures; no live EIA validation is claimed. The separately fetched headline is not presented as the cause of the FX change.
+
+The [additional deterministic record](hosted-verification-final.json) verifies health/proof binding, tool listing, fixture computation and invalid-input behavior. The [source comparison record](source-verification-final.json) verifies the public 76-file archive for the exact commit, allowing text line-ending differences only. Original dependency notices and applicable source terms are retained under [licenses](licenses/DEPENDENCIES.md).
+
+## 5. Official package validation
+
+From a clean official-repository checkout, execute before publishing:
+
+```bash
+npm ci
+npm run validate:submission -- --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate
+npm run validate:submission -- --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate --online
+```
+
+Both official commands passed on **2026-09-17 at 07:03 UTC**. The [archived validation record](official-validation.json) retains the actual command lines, start/end timestamps, exit codes, original stdout/stderr, parsed results and the SHA-256 of the unmodified official validator. Offline validation confirmed the manifest, 73 source files, baseline secret scan and repeatable verification instructions. Online validation additionally verified the public source commit, healthy deployed API bound to that commit, and standard deployment proof. TLS verification was unchanged.
+
+The inspected official validator uses a 60-second request timeout, no redirects/retries and a 64 KiB response cap. The archived record captures the checks immediately before adding that record and this summary. After adding them, both commands are run again on the complete package; that final output is retained outside the submission package to avoid a self-referential evidence update. Final diff review must remain limited to this one submission directory. Passing these checks does not establish a security certification, program acceptance or award.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-live-verification-final.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-live-verification-final.json
new file mode 100644
index 0000000..7c08d4f
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-live-verification-final.json
@@ -0,0 +1,704 @@
+{
+ "recorded_at": "2026-09-16T09:55:43.203540+00:00",
+ "origin": "https://life-exchange-rate-mcp.vercel.app",
+ "endpoint": "https://life-exchange-rate-mcp.vercel.app/mcp/",
+ "expected_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "transport": "Public HTTPS Streamable HTTP on Vercel",
+ "live_provider_calls": true,
+ "before": {
+ "health": {
+ "status": "ok",
+ "version": "0.2.0",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "mcp": "enabled"
+ },
+ "proof": {
+ "schemaVersion": 1,
+ "slug": "kongkoukk-life-exchange-rate",
+ "version": "0.2.0",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "health": "https://life-exchange-rate-mcp.vercel.app/health",
+ "openapi": "https://life-exchange-rate-mcp.vercel.app/openapi.json",
+ "mcp": "https://life-exchange-rate-mcp.vercel.app/mcp/"
+ }
+ },
+ "modes": [
+ {
+ "mode": "auto",
+ "protocol_version": "2026-07-28",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ },
+ {
+ "mode": "legacy",
+ "protocol_version": "2025-11-25",
+ "server_info": {
+ "name": "Life Exchange Rate",
+ "title": null,
+ "version": "0.2.0",
+ "description": null,
+ "website_url": null,
+ "icons": null
+ },
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "get_policy_rate_event",
+ "status": "passed"
+ },
+ {
+ "tool": "get_energy_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "compare_event_scenarios",
+ "status": "passed"
+ },
+ {
+ "tool": "get_official_macro_headlines",
+ "status": "passed"
+ },
+ {
+ "tool": "get_live_fx_event",
+ "status": "passed"
+ },
+ {
+ "tool": "rank_events_for_user",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ }
+ ],
+ "negative_checks": [
+ {
+ "check": "unsupported event mode",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "unknown headline source",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "identical FX currencies",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "out-of-range lookback",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "headline lacks numeric event values",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "conversion currency mismatch",
+ "status": "rejected_as_expected"
+ },
+ {
+ "check": "invalid scenario multiplier",
+ "status": "rejected_as_expected"
+ }
+ ]
+ }
+ ],
+ "end_to_end": {
+ "event_mode": "live_official_observations",
+ "profile_origin": "Explicit synthetic judging profile; prices and income are inputs, not claimed market observations.",
+ "profile": {
+ "home_currency": "SEK",
+ "home_country": "SE",
+ "net_monthly_income": 32000.0,
+ "monthly_work_hours": 160.0,
+ "travel_budget_home": 20000.0,
+ "travel_target_currency": "JPY",
+ "mortgage": {
+ "principal": 2500000.0,
+ "remaining_years": 25.0,
+ "current_annual_rate_pct": 3.8,
+ "currency": "SEK"
+ },
+ "savings_balance": 100000.0,
+ "commute": {
+ "monthly_fuel_liters": 65.0,
+ "fuel_price_per_liter": 18.5
+ },
+ "life_units": [
+ {
+ "name": "beer",
+ "price": 80.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "coffee",
+ "price": 45.0,
+ "currency": "SEK"
+ },
+ {
+ "name": "lunch",
+ "price": 140.0,
+ "currency": "SEK"
+ }
+ ]
+ },
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-15T00:00:00Z",
+ "old_value": 16.0689,
+ "new_value": 15.8648,
+ "unit": "JPY per SEK",
+ "change_pct": -1.2701553933374354,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-08T00:00:00Z",
+ "window_end": "2026-09-15T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T09:55:46.355190Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-08",
+ "new_date": "2026-09-15",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-08",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 1,
+ "query_start_date": "2025-08-18",
+ "query_end_date": "2026-09-16",
+ "history_start_date": "2025-08-18",
+ "pre_query_observation_count": 0,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-29",
+ "reference_window_end": "2026-09-08",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.012782908288157646,
+ "reference_mean_log_return": 0.0005772762267621959,
+ "reference_sample_stddev": 0.011447140325383607,
+ "z_score": -1.1671198338762503,
+ "fallback_reason": null
+ },
+ "z_score": -1.1671198338762503
+ }
+ },
+ "ranked_radar": [
+ {
+ "event": {
+ "event_id": "synthetic-brent-2025-12-24-2025-12-31",
+ "event_type": "oil_move",
+ "title": "Synthetic scenario: Brent crude moved +15.00% over the observation window",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 80.0,
+ "new_value": 92.0,
+ "unit": "USD per barrel",
+ "change_pct": 14.999999999999991,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [],
+ "jurisdiction": null,
+ "evidence_url": null,
+ "window_start": "2025-12-24T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-16T09:55:44.738824Z",
+ "series_id": "synthetic-brent-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not EIA observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 7,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-24",
+ "old_date": "2025-12-24",
+ "new_date": "2025-12-31",
+ "actual_window_days": 7,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "commodity": "Brent crude oil",
+ "price_currency": "USD",
+ "scope": "Brent Europe spot benchmark",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-16"
+ }
+ },
+ "market_significance": 90.0,
+ "user_relevance": 100.0,
+ "priority_score": 95.5,
+ "significance_method": "fallback_oil_percent_move",
+ "relevance_reasons": [
+ "User supplied monthly fuel use and pump price",
+ "User has direct driving exposure"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "event_type": "fx_move",
+ "title": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "source": "Frankfurter / ECB",
+ "observed_at": "2026-09-15T00:00:00Z",
+ "old_value": 16.0689,
+ "new_value": 15.8648,
+ "unit": "JPY per SEK",
+ "change_pct": -1.2701553933374354,
+ "base_currency": "SEK",
+ "quote_currency": "JPY",
+ "affected_currencies": [
+ "SEK",
+ "JPY"
+ ],
+ "jurisdiction": null,
+ "evidence_url": "https://frankfurter.dev/providers/ecb/",
+ "window_start": "2026-09-08T00:00:00Z",
+ "window_end": "2026-09-15T00:00:00Z",
+ "confidence": "observed",
+ "provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T09:55:46.355190Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "metadata": {
+ "old_date": "2026-09-08",
+ "new_date": "2026-09-15",
+ "provider": "ECB",
+ "requested_lookback_days": 7,
+ "target_old_date": "2026-09-08",
+ "actual_calendar_days": 7,
+ "anchor_gap_days": 0,
+ "latest_observation_age_days": 1,
+ "query_start_date": "2025-08-18",
+ "query_end_date": "2026-09-16",
+ "history_start_date": "2025-08-18",
+ "pre_query_observation_count": 0,
+ "query_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "history_observation_count": 276,
+ "identical_duplicates_removed": 0,
+ "anomaly": {
+ "method": "preceding_equal_calendar_horizon_log_return_z_score",
+ "formula": "(event_log_return - reference_mean_log_return) / reference_sample_stddev",
+ "horizon_calendar_days": 7,
+ "sample_count": 252,
+ "minimum_sample_count": 30,
+ "maximum_sample_count": 252,
+ "reference_window_start": "2025-08-29",
+ "reference_window_end": "2026-09-08",
+ "reference_windows_may_overlap": true,
+ "event_excluded_from_reference": true,
+ "missing_endpoint_policy": "skip_window_without_exact_calendar_horizon_endpoints",
+ "event_log_return": -0.012782908288157646,
+ "reference_mean_log_return": 0.0005772762267621959,
+ "reference_sample_stddev": 0.011447140325383607,
+ "z_score": -1.1671198338762503,
+ "fallback_reason": null
+ },
+ "z_score": -1.1671198338762503
+ }
+ },
+ "market_significance": 29.2,
+ "user_relevance": 100.0,
+ "priority_score": 68.1,
+ "significance_method": "provider_z_score",
+ "relevance_reasons": [
+ "FX pair includes the user's home currency",
+ "FX pair includes the user's travel target currency",
+ "User supplied a travel budget"
+ ]
+ },
+ {
+ "event": {
+ "event_id": "synthetic-deposit-rate-2025-12-01-2025-12-31",
+ "event_type": "interest_rate_change",
+ "title": "Synthetic scenario: Euro-area deposit facility rate moved +25.0 basis points",
+ "source": "synthetic judging fixture",
+ "observed_at": "2025-12-31T00:00:00Z",
+ "old_value": 3.0,
+ "new_value": 3.25,
+ "unit": "percent",
+ "change_pct": null,
+ "base_currency": null,
+ "quote_currency": null,
+ "affected_currencies": [
+ "EUR"
+ ],
+ "jurisdiction": "EA",
+ "evidence_url": null,
+ "window_start": "2025-12-01T00:00:00Z",
+ "window_end": "2025-12-31T00:00:00Z",
+ "confidence": "scenario",
+ "provenance": {
+ "provider": "synthetic-fixture",
+ "source_type": "synthetic_fixture",
+ "retrieved_at": "2026-09-16T09:55:44.589454Z",
+ "series_id": "synthetic-policy-rate-step-v1",
+ "source_url": null,
+ "notes": [
+ "Synthetic fixed step scenario; values and dates are not ECB observations."
+ ]
+ },
+ "metadata": {
+ "requested_lookback_days": 30,
+ "requested_as_of": "2025-12-31",
+ "target_start_date": "2025-12-01",
+ "old_date": "2025-12-01",
+ "new_date": "2025-12-31",
+ "actual_window_days": 30,
+ "observation_lag_days": 0,
+ "window_method": "Latest published observation and last observation on/before its date minus lookback_days; baseline tolerance 7 days.",
+ "date_precision": "day; UTC midnight encodes the source date, not an intraday publication timestamp",
+ "provider": "synthetic-fixture",
+ "synthetic": true,
+ "mode": "fixture",
+ "fallback_used": false,
+ "change_basis_points": 25.0,
+ "change_percentage_points": 0.25,
+ "scope": "Euro area; EUR exposures only",
+ "rate_name": "deposit facility",
+ "fixture_anchor_date": "2025-12-31",
+ "live_request_as_of": "2026-09-16"
+ }
+ },
+ "market_significance": 45.0,
+ "user_relevance": 0.0,
+ "priority_score": 20.2,
+ "significance_method": "fallback_policy_rate_basis_points",
+ "relevance_reasons": [
+ "Policy-rate currency scope does not support a direct home-currency impact"
+ ]
+ }
+ ],
+ "impact": {
+ "event_provenance": {
+ "provider": "Frankfurter (ECB reference-rate provider)",
+ "source_type": "structured_intermediary_official_provider",
+ "retrieved_at": "2026-09-16T09:55:46.355190Z",
+ "series_id": "ECB:SEK/JPY",
+ "source_url": "https://api.frankfurter.dev/v2/providers/ecb/rates?from=2025-08-18&to=2026-09-16&base=SEK"es=JPY",
+ "notes": [
+ "Query uses the dedicated ECB provider route; observations are daily reference rates, not executable quotes.",
+ "Up to seven calendar days of provider backfill before the query start are retained and counted separately.",
+ "Observation timestamps represent source dates at 00:00 UTC, not publication times.",
+ "Requested 7 calendar days; actual observed window is 2026-09-08 through 2026-09-15.",
+ "Anomaly reference windows end on or before the event start; overlapping reference windows are descriptive, not independent trials."
+ ]
+ },
+ "event_observed_at": "2026-09-15T00:00:00Z",
+ "event_id": "fx-SEK-JPY-2026-09-08-2026-09-15",
+ "headline": "SEK/JPY moved -1.27% from 2026-09-08 to 2026-09-15",
+ "direct_effect_home": 257.3,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 1.29,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 3.22,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 5.72,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 1.84,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 257.30 SEK more than before.",
+ "confidence": "observed",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.0689
+ },
+ "result": 321378.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 15.8648
+ },
+ "result": 317296.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 16.0689,
+ "new_fx_rate": 15.8648
+ },
+ "result": 20257.3
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 20257.3,
+ "travel_budget_home": 20000.0
+ },
+ "result": 257.3
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 321378.0,
+ "new_target_purchasing_power": 317296.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ },
+ "life_unit_conversion": {
+ "amount": 257.3,
+ "currency": "SEK",
+ "work_hours_equivalent": 1.29,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 3.22,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 5.72,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 1.84,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ]
+ },
+ "separately_fetched_official_headline": {
+ "headline_id": "fed-9014ded4c9180ac8",
+ "title": "Minutes of the Board's discount rate meetings on July 20 and July 29, 2026",
+ "publisher": "Federal Reserve Board",
+ "published_at": "2026-08-25T18:00:00Z",
+ "url": "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260825a.htm",
+ "jurisdiction": "US",
+ "event_type_hint": null,
+ "tags": [],
+ "requires_quantification": true,
+ "source_type": "official_rss"
+ },
+ "headline_relationship": "Shown to test the headline boundary; no causal link to the FX move is asserted. Only the numeric FX observations drive this calculation."
+ },
+ "after": {
+ "health": {
+ "status": "ok",
+ "version": "0.2.0",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "mcp": "enabled"
+ },
+ "proof": {
+ "schemaVersion": 1,
+ "slug": "kongkoukk-life-exchange-rate",
+ "version": "0.2.0",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "health": "https://life-exchange-rate-mcp.vercel.app/health",
+ "openapi": "https://life-exchange-rate-mcp.vercel.app/openapi.json",
+ "mcp": "https://life-exchange-rate-mcp.vercel.app/mcp/"
+ }
+ },
+ "status": "passed"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-rest-verification-final.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-rest-verification-final.json
new file mode 100644
index 0000000..13b76ba
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-rest-verification-final.json
@@ -0,0 +1,195 @@
+{
+ "recorded_at": "2026-09-16T10:00:54.306660+00:00",
+ "origin": "https://life-exchange-rate-mcp.vercel.app",
+ "expected_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "synthetic_profile": true,
+ "redirects_allowed": false,
+ "status": "passed",
+ "positive": {
+ "http_status": 200,
+ "response": {
+ "event_provenance": {
+ "provider": "demo-fixture",
+ "source_type": "fixture",
+ "retrieved_at": "2026-09-10T16:00:00Z",
+ "series_id": null,
+ "source_url": null,
+ "notes": [
+ "Synthetic scenario; values and z-score are illustrative, not official observations."
+ ]
+ },
+ "event_observed_at": "2026-09-10T16:00:00Z",
+ "event_id": "demo-jpy-sek-8pct",
+ "headline": "JPY strengthens sharply against SEK",
+ "direct_effect_home": 1739.13,
+ "home_currency": "SEK",
+ "direction": "cost_increase",
+ "impact_horizon": "one_off_travel_budget",
+ "work_hours_equivalent": 8.7,
+ "life_units": [
+ {
+ "unit": "beer",
+ "quantity": 21.74,
+ "currency": "SEK",
+ "unit_price": 80.0
+ },
+ {
+ "unit": "coffee",
+ "quantity": 38.65,
+ "currency": "SEK",
+ "unit_price": 45.0
+ },
+ {
+ "unit": "lunch",
+ "quantity": 12.42,
+ "currency": "SEK",
+ "unit_price": 140.0
+ }
+ ],
+ "explanation": "To preserve the same JPY purchasing power for the trip, the user would need about 1739.13 SEK more than before.",
+ "confidence": "scenario",
+ "assumptions": [
+ "Rates in the trace are destination-currency units per home-currency unit.",
+ "Reference-rate comparison excludes retail exchange fees and spreads."
+ ],
+ "warnings": [
+ "This event uses a synthetic judging fixture, not a current market observation."
+ ],
+ "calculation_trace": [
+ {
+ "label": "old destination purchasing power",
+ "formula": "travel_budget_home × old_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0
+ },
+ "result": 300000.0
+ },
+ {
+ "label": "new destination purchasing power",
+ "formula": "travel_budget_home × new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 276000.0
+ },
+ {
+ "label": "budget required to restore old purchasing power",
+ "formula": "travel_budget_home × old_fx_rate / new_fx_rate",
+ "inputs": {
+ "travel_budget_home": 20000.0,
+ "old_fx_rate": 15.0,
+ "new_fx_rate": 13.8
+ },
+ "result": 21739.13
+ },
+ {
+ "label": "personal impact",
+ "formula": "restore_budget - travel_budget_home",
+ "inputs": {
+ "restore_budget": 21739.13,
+ "travel_budget_home": 20000.0
+ },
+ "result": 1739.13
+ }
+ ],
+ "details": {
+ "old_target_purchasing_power": 300000.0,
+ "new_target_purchasing_power": 276000.0,
+ "target_currency": "JPY",
+ "travel_budget_home": 20000.0
+ }
+ }
+ },
+ "negative": {
+ "http_status": 422,
+ "response": {
+ "detail": [
+ {
+ "loc": [
+ "body",
+ "event",
+ "event_id"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "event",
+ "title"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "event",
+ "source"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "event",
+ "old_value"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "event",
+ "new_value"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "event",
+ "unit"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "profile",
+ "home_currency"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "profile",
+ "net_monthly_income"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ },
+ {
+ "loc": [
+ "body",
+ "profile",
+ "monthly_work_hours"
+ ],
+ "msg": "Field required",
+ "type": "missing"
+ }
+ ]
+ }
+ },
+ "commit_before": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "commit_after": "238954196ea6922d3cea2ae8acb914b0f4e01dbb"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification-final.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification-final.json
new file mode 100644
index 0000000..af41e4d
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification-final.json
@@ -0,0 +1,125 @@
+{
+ "recorded_at": "2026-09-16T09:55:21.884675+00:00",
+ "status": "passed",
+ "origin": "https://life-exchange-rate-mcp.vercel.app",
+ "expected_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "expected_slug": "kongkoukk-life-exchange-rate",
+ "sdk_version": "2.2.0",
+ "timeout_seconds": 10,
+ "health_and_proof_redirects_allowed": false,
+ "mcp_redirect_policy": "SDK 2.2.0: same-origin, method-preserving redirects only",
+ "live_provider_calls": false,
+ "profile_origin": "Synthetic judging inputs; no market observations claimed.",
+ "stage": "complete",
+ "health": {
+ "status": "passed",
+ "http_status": 200,
+ "elapsed_ms": 454,
+ "response_bytes": 101,
+ "status_value": "ok",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "mcp": "enabled"
+ },
+ "deployment_proof": {
+ "status": "passed",
+ "http_status": 200,
+ "elapsed_ms": 140,
+ "response_bytes": 366,
+ "slug": "kongkoukk-life-exchange-rate",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "origin_matches": true,
+ "mcp_url_matches": true
+ },
+ "modes": [
+ {
+ "mode": "auto",
+ "protocol_version": "2026-07-28",
+ "initialize": "passed",
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "get_macro_events",
+ "status": "invalid_input_rejected"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "invalid_input_rejected"
+ }
+ ],
+ "fixture_result": {
+ "event_id": "demo-jpy-sek-8pct",
+ "direct_effect_home": 1739.13,
+ "currency": "SEK",
+ "work_hours": 8.7,
+ "calculation_trace_present": true
+ }
+ },
+ {
+ "mode": "legacy",
+ "protocol_version": "2025-11-25",
+ "initialize": "passed",
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "get_macro_events",
+ "status": "invalid_input_rejected"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "invalid_input_rejected"
+ }
+ ],
+ "fixture_result": {
+ "event_id": "demo-jpy-sek-8pct",
+ "direct_effect_home": 1739.13,
+ "currency": "SEK",
+ "work_hours": 8.7,
+ "calculation_trace_present": true
+ }
+ }
+ ]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification.json
new file mode 100644
index 0000000..37691d3
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/hosted-verification.json
@@ -0,0 +1,125 @@
+{
+ "recorded_at": "2026-09-16T18:37:20.874728+00:00",
+ "status": "passed",
+ "origin": "https://life-exchange-rate-mcp.vercel.app",
+ "expected_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "expected_slug": "kongkoukk-life-exchange-rate",
+ "sdk_version": "2.2.0",
+ "timeout_seconds": 10,
+ "health_and_proof_redirects_allowed": false,
+ "mcp_redirect_policy": "SDK 2.2.0: same-origin, method-preserving redirects only",
+ "live_provider_calls": false,
+ "profile_origin": "Synthetic judging inputs; no market observations claimed.",
+ "stage": "complete",
+ "health": {
+ "status": "passed",
+ "http_status": 200,
+ "elapsed_ms": 3015,
+ "response_bytes": 101,
+ "status_value": "ok",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "mcp": "enabled"
+ },
+ "deployment_proof": {
+ "status": "passed",
+ "http_status": 200,
+ "elapsed_ms": 157,
+ "response_bytes": 366,
+ "slug": "kongkoukk-life-exchange-rate",
+ "commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "origin_matches": true,
+ "mcp_url_matches": true
+ },
+ "modes": [
+ {
+ "mode": "auto",
+ "protocol_version": "2026-07-28",
+ "initialize": "passed",
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "get_macro_events",
+ "status": "invalid_input_rejected"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "invalid_input_rejected"
+ }
+ ],
+ "fixture_result": {
+ "event_id": "demo-jpy-sek-8pct",
+ "direct_effect_home": 1739.13,
+ "currency": "SEK",
+ "work_hours": 8.7,
+ "calculation_trace_present": true
+ }
+ },
+ {
+ "mode": "legacy",
+ "protocol_version": "2025-11-25",
+ "initialize": "passed",
+ "tool_names": [
+ "compare_event_scenarios",
+ "convert_to_life_units",
+ "get_energy_event",
+ "get_live_fx_event",
+ "get_macro_events",
+ "get_official_macro_headlines",
+ "get_policy_rate_event",
+ "rank_events_for_user",
+ "translate_event_to_life"
+ ],
+ "calls": [
+ {
+ "tool": "get_macro_events",
+ "status": "passed"
+ },
+ {
+ "tool": "translate_event_to_life",
+ "status": "passed"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "passed"
+ },
+ {
+ "tool": "get_macro_events",
+ "status": "invalid_input_rejected"
+ },
+ {
+ "tool": "convert_to_life_units",
+ "status": "invalid_input_rejected"
+ }
+ ],
+ "fixture_result": {
+ "event_id": "demo-jpy-sek-8pct",
+ "direct_effect_home": 1739.13,
+ "currency": "SEK",
+ "work_hours": 8.7,
+ "calculation_trace_present": true
+ }
+ }
+ ]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/DEPENDENCIES.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/DEPENDENCIES.md
new file mode 100644
index 0000000..544e4fc
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/DEPENDENCIES.md
@@ -0,0 +1,183 @@
+# 第三方依赖与许可清单
+
+整理日期:2026-09-16。本地清单依据项目锁文件、构建配置,以及已检查测试环境中安装发行包的 `dist-info/METADATA` 和许可证文件。补充官方来源声明见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md):已读取 uvloop、httpx2-jsfetch 的版本许可声明、Hatch 上游许可及数据来源条款。保留本地证据和网页证据的区别,没有安装新依赖,也没有替第一方项目选择或授予许可证。
+
+## 范围与本地证据
+
+项目:Life Exchange Rate MCP 0.2.0。根运行时声明为 `fastapi`、`httpx`、`mcp[cli]`、`pydantic`、`uvicorn[standard]`;开发额外依赖为 `pytest` 与 `pytest-asyncio`。所有第三方锁定条目的注册源均为 `https://pypi.org/simple`;具体构件 URL 与哈希保留在 `source/uv.lock`。
+
+- 锁文件:`source/uv.lock`。SHA-256:`d67c058ded87d2778474f63fe051214025122c77982f80baf1e7bee1c94a4e5d`。
+- 项目配置:`source/pyproject.toml`。
+- 表格“本地元数据”路径以已检查测试环境的 `site-packages/` 为相对根目录;不包含个人机器路径。
+- [THIRD_PARTY_NOTICES.txt](THIRD_PARTY_NOTICES.txt) 汇总实际读到的 68 份许可、版权或作者文本。每份均保留原相对路径标题、SHA-256、原文长度及完整原始字节;没有生成或替换上游许可。原文不能因表内简写而被忽略。
+
+本表按锁文件依赖边和启用的 extras 计算传递闭包,列出 **48 个运行时第三方锁定发行包**,包括平台条件依赖;另有 **5 个仅开发依赖**。它是支持平台的联合清单,不表示这些包全部同时安装在 Vercel Linux 或本地 Windows。元数据仅在本地安装版本与锁定版本完全一致时用于许可证明。
+
+上游 Python 依赖通过正常依赖安装取得;它们的实现源码、虚拟环境及 wheel 并未随项目源码复制。本次审阅附带的是读取到的许可/通知文本。依赖声明不等于代替上游授予权利,本清单也未审计所有 wheel 内嵌的原生库或平台运行时。
+
+## 锁定运行时依赖
+
+“直接”表示来自根项目运行时声明;其余为传递依赖。许可名称按实际元数据记录保留,没有将旧式名称强行换成未经确认的 SPDX 标识。
+
+| 包 | 锁定版本 | 来源层级/条件 | 本地许可记录 | 本地元数据 | 原文副本 |
+|---|---|---|---|---|---|
+| annotated-doc | 0.0.5 | 传递 | MIT | `annotated_doc-0.0.5.dist-info/METADATA`(License-Expression) | [dependency-notices/annotated_doc-0.0.5.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| annotated-types | 0.8.0 | 传递 | MIT | `annotated_types-0.8.0.dist-info/METADATA`(License-Expression) | [dependency-notices/annotated_types-0.8.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| anyio | 4.15.1 | 传递 | MIT | `anyio-4.15.1.dist-info/METADATA`(License-Expression) | [dependency-notices/anyio-4.15.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| attrs | 26.1.0 | 传递 | MIT | `attrs-26.1.0.dist-info/METADATA`(License-Expression) | [dependency-notices/attrs-26.1.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| certifi | 2026.7.22 | 传递 | MPL-2.0 | `certifi-2026.7.22.dist-info/METADATA`(License) | [dependency-notices/certifi-2026.7.22.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| cffi | 2.1.1 | 传递 | MIT-0 | `cffi-2.1.1.dist-info/METADATA`(License-Expression) | [dependency-notices/cffi-2.1.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| click | 8.5.0 | 传递 | BSD-3-Clause | `click-8.5.0.dist-info/METADATA`(License-Expression) | [dependency-notices/click-8.5.0.dist-info/licenses/LICENSE.txt](THIRD_PARTY_NOTICES.txt) |
+| colorama | 0.4.6 | 传递;Windows 条件 | BSD License(分类器);许可原文含三项条件 | `colorama-0.4.6.dist-info/METADATA`(Classifier + LICENSE.txt) | [dependency-notices/colorama-0.4.6.dist-info/licenses/LICENSE.txt](THIRD_PARTY_NOTICES.txt) |
+| cryptography | 50.0.1 | 传递 | Apache-2.0 OR BSD-3-Clause | `cryptography-50.0.1.dist-info/METADATA`(License-Expression) | [dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD](THIRD_PARTY_NOTICES.txt) |
+| fastapi | 0.141.1 | 直接 | MIT | `fastapi-0.141.1.dist-info/METADATA`(License-Expression) | [dependency-notices/fastapi-0.141.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| h11 | 0.16.0 | 传递 | MIT | `h11-0.16.0.dist-info/METADATA`(License) | [dependency-notices/h11-0.16.0.dist-info/licenses/LICENSE.txt](THIRD_PARTY_NOTICES.txt) |
+| httpcore | 1.0.9 | 传递 | BSD-3-Clause | `httpcore-1.0.9.dist-info/METADATA`(License-Expression) | [dependency-notices/httpcore-1.0.9.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| httpcore2 | 2.12.0 | 传递 | BSD-3-Clause | `httpcore2-2.12.0.dist-info/METADATA`(License-Expression) | [dependency-notices/httpcore2-2.12.0.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| httptools | 0.8.0 | 传递 | MIT | `httptools-0.8.0.dist-info/METADATA`(License-Expression) | [dependency-notices/httptools-0.8.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/http-parser/LICENSE-MIT](THIRD_PARTY_NOTICES.txt)
[dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/llhttp/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| httpx | 0.28.1 | 直接 | BSD-3-Clause | `httpx-0.28.1.dist-info/METADATA`(License) | [dependency-notices/httpx-0.28.1.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| httpx2 | 2.12.0 | 传递 | BSD-3-Clause | `httpx2-2.12.0.dist-info/METADATA`(License-Expression) | [dependency-notices/httpx2-2.12.0.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| httpx2-jsfetch | 1.0 | 传递;Python ≥3.12 且 Emscripten | 本地无该版本元数据/原文;上游发布页声明见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md) | 未找到匹配版本 | — |
+| idna | 3.19 | 传递 | BSD-3-Clause | `idna-3.19.dist-info/METADATA`(License-Expression) | [dependency-notices/idna-3.19.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| jsonschema | 4.26.0 | 传递 | MIT | `jsonschema-4.26.0.dist-info/METADATA`(License-Expression) | [dependency-notices/jsonschema-4.26.0.dist-info/licenses/COPYING](THIRD_PARTY_NOTICES.txt) |
+| jsonschema-specifications | 2025.9.1 | 传递 | MIT | `jsonschema_specifications-2025.9.1.dist-info/METADATA`(License-Expression) | [dependency-notices/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING](THIRD_PARTY_NOTICES.txt) |
+| markdown-it-py | 4.2.0 | 传递 | MIT License | `markdown_it_py-4.2.0.dist-info/METADATA`(Classifier) | [dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE.markdown-it](THIRD_PARTY_NOTICES.txt) |
+| mcp | 2.2.0 | 直接 | MIT | `mcp-2.2.0.dist-info/METADATA`(License) | [dependency-notices/mcp-2.2.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| mcp-types | 2.2.0 | 传递 | MIT | `mcp_types-2.2.0.dist-info/METADATA`(License) | [dependency-notices/mcp_types-2.2.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| mdurl | 0.1.2 | 传递 | MIT License | `mdurl-0.1.2.dist-info/METADATA`(Classifier) | [dependency-notices/mdurl-0.1.2.dist-info/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| opentelemetry-api | 1.44.0 | 传递 | Apache-2.0 | `opentelemetry_api-1.44.0.dist-info/METADATA`(License-Expression) | [dependency-notices/opentelemetry_api-1.44.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pycparser | 3.0 | 传递 | BSD-3-Clause | `pycparser-3.0.dist-info/METADATA`(License-Expression) | [dependency-notices/pycparser-3.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pydantic | 2.13.5 | 直接 | MIT | `pydantic-2.13.5.dist-info/METADATA`(License-Expression) | [dependency-notices/pydantic-2.13.5.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pydantic-core | 2.46.5 | 传递 | MIT | `pydantic_core-2.46.5.dist-info/METADATA`(License-Expression) | [dependency-notices/pydantic_core-2.46.5.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pygments | 2.21.0 | 传递 | BSD-2-Clause | `pygments-2.21.0.dist-info/METADATA`(License-Expression) | [dependency-notices/pygments-2.21.0.dist-info/licenses/AUTHORS](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pygments-2.21.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pyjwt | 2.14.0 | 传递 | MIT | `pyjwt-2.14.0.dist-info/METADATA`(License-Expression) | [dependency-notices/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pyjwt-2.14.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| python-dotenv | 1.2.3 | 传递 | BSD-3-Clause | `python_dotenv-1.2.3.dist-info/METADATA`(License) | [dependency-notices/python_dotenv-1.2.3.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| python-multipart | 0.0.32 | 传递 | Apache-2.0 | `python_multipart-0.0.32.dist-info/METADATA`(License-Expression) | [dependency-notices/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt](THIRD_PARTY_NOTICES.txt) |
+| pywin32 | 312 | 传递;`sys_platform == win32` | PSF(顶层元数据);含 LGPL v2.1 等组件原文,见下文 | `pywin32-312.dist-info/METADATA`(License + 多个组件许可) | [dependency-notices/pywin32-312.dist-info/licenses/adodbapi/license.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/com/License.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/com/win32comext/mapi/src/MAPIStubLibrary/LICENSE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/isapi/README.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/pythonwin/License.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/pythonwin/pywin/idle/LICENSE.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/pythonwin/Scintilla/License.txt](THIRD_PARTY_NOTICES.txt)
[dependency-notices/pywin32-312.dist-info/licenses/win32/License.txt](THIRD_PARTY_NOTICES.txt) |
+| pyyaml | 6.0.3 | 传递 | MIT | `pyyaml-6.0.3.dist-info/METADATA`(License) | [dependency-notices/pyyaml-6.0.3.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| referencing | 0.37.0 | 传递 | MIT | `referencing-0.37.0.dist-info/METADATA`(License-Expression) | [dependency-notices/referencing-0.37.0.dist-info/licenses/COPYING](THIRD_PARTY_NOTICES.txt) |
+| rich | 15.0.0 | 传递 | MIT | `rich-15.0.0.dist-info/METADATA`(License) | [dependency-notices/rich-15.0.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| rpds-py | 2026.6.3 | 传递 | MIT | `rpds_py-2026.6.3.dist-info/METADATA`(License-Expression) | [dependency-notices/rpds_py-2026.6.3.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| shellingham | 1.5.4 | 传递 | ISC License | `shellingham-1.5.4.dist-info/METADATA`(License) | [dependency-notices/shellingham-1.5.4.dist-info/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| sse-starlette | 3.4.11 | 传递 | BSD-3-Clause | `sse_starlette-3.4.11.dist-info/METADATA`(License-Expression) | [dependency-notices/sse_starlette-3.4.11.dist-info/licenses/AUTHORS](THIRD_PARTY_NOTICES.txt)
[dependency-notices/sse_starlette-3.4.11.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| starlette | 1.6.0 | 传递 | BSD-3-Clause | `starlette-1.6.0.dist-info/METADATA`(License-Expression) | [dependency-notices/starlette-1.6.0.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| truststore | 0.10.4 | 传递 | MIT | `truststore-0.10.4.dist-info/METADATA`(License-Expression) | [dependency-notices/truststore-0.10.4.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| typer | 0.27.2 | 传递 | MIT | `typer-0.27.2.dist-info/METADATA`(License-Expression) | [dependency-notices/typer-0.27.2.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| typing-extensions | 4.16.0 | 传递 | PSF-2.0 | `typing_extensions-4.16.0.dist-info/METADATA`(License-Expression) | [dependency-notices/typing_extensions-4.16.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| typing-inspection | 0.4.4 | 传递 | MIT | `typing_inspection-0.4.4.dist-info/METADATA`(License-Expression) | [dependency-notices/typing_inspection-0.4.4.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| uvicorn | 0.53.0 | 直接 | BSD-3-Clause | `uvicorn-0.53.0.dist-info/METADATA`(License-Expression) | [dependency-notices/uvicorn-0.53.0.dist-info/licenses/LICENSE.md](THIRD_PARTY_NOTICES.txt) |
+| uvloop | 0.22.1 | 传递;非 PyPy、非 Cygwin、非 Windows | 本地无该版本元数据/原文;上游发布页声明见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md) | 未找到匹配版本 | — |
+| watchfiles | 1.2.0 | 传递 | MIT | `watchfiles-1.2.0.dist-info/METADATA`(License) | [dependency-notices/watchfiles-1.2.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| websockets | 17.1 | 传递 | BSD-3-Clause | `websockets-17.1.dist-info/METADATA`(License-Expression) | [dependency-notices/websockets-17.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+
+## 仅开发依赖
+
+下列包仅来自 `dev` 额外依赖。开发与运行时共享的 `colorama`, `pygments`, `typing-extensions` 已在运行时表中列出,不重复计数。
+
+| 包 | 锁定版本 | 来源层级 | 本地许可记录 | 本地元数据 | 原文副本 |
+|---|---|---|---|---|---|
+| iniconfig | 2.3.0 | 传递 dev | MIT | `iniconfig-2.3.0.dist-info/METADATA`(License-Expression) | [dependency-notices/iniconfig-2.3.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| packaging | 26.3 | 传递 dev | Apache-2.0 OR BSD-2-Clause | `packaging-26.3.dist-info/METADATA`(License-Expression) | [dependency-notices/packaging-26.3.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.APACHE](THIRD_PARTY_NOTICES.txt)
[dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.BSD](THIRD_PARTY_NOTICES.txt) |
+| pluggy | 1.6.0 | 传递 dev | MIT | `pluggy-1.6.0.dist-info/METADATA`(License) | [dependency-notices/pluggy-1.6.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pytest | 9.1.1 | 直接 dev | MIT | `pytest-9.1.1.dist-info/METADATA`(License-Expression) | [dependency-notices/pytest-9.1.1.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+| pytest-asyncio | 1.4.0 | 直接 dev | Apache-2.0 | `pytest_asyncio-1.4.0.dist-info/METADATA`(License-Expression) | [dependency-notices/pytest_asyncio-1.4.0.dist-info/licenses/LICENSE](THIRD_PARTY_NOTICES.txt) |
+
+## 构建依赖与未确认项
+
+- `hatchling` 是 `source/pyproject.toml` 声明的构建后端;本锁文件没有锁定其版本,已检查测试环境未安装它。已读取 Hatch 当前上游 MIT 许可,见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md);实际隔离构建后端版本及其对应构件仍未核实,不把它冒充锁定运行时依赖。
+- `uvloop 0.22.1`:锁文件包含非 Windows 条件依赖。本地 Windows 测试环境没有匹配的已安装发行包;其版本发布页声明 MIT/Apache-2.0 双许可,见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md)。尚未核对 Linux 部署安装构件或复制该构件许可原文,因此不能把本地表当作完整的线上许可凭证。
+- `httpx2-jsfetch 1.0`:锁文件中的 Emscripten 条件依赖,本地 Windows 测试环境未安装;其版本发布页声明 BSD-3-Clause,见 [PROVIDER-TERMS.md](PROVIDER-TERMS.md)。尚未取得对应安装构件的许可原文。
+- 这里只确认锁文件与本地元数据,未读取 Vercel 上的安装目录;线上实际构件、原生组件和构建工具应在需要完整部署软件物料清单时另行核对。
+
+## 多组件许可需要保留原文
+
+`pywin32 312` 的顶层元数据写作 `PSF`,但本地随包许可不能概括为“全部 PSF”:
+
+- `licenses/adodbapi/license.txt` 是 **GNU Lesser General Public License, Version 2.1, February 1999** 的文本。该文件本身不足以确认所有相关组件是否有 “or later” 授权,因此不另行推断 SPDX 后缀。
+- `licenses/com/License.txt`、`licenses/pythonwin/License.txt`、`licenses/win32/License.txt` 含各自版权主体、三项再分发条件及免责声明。
+- `licenses/com/win32comext/mapi/src/MAPIStubLibrary/LICENSE` 明示 MIT;Scintilla、IDLE/Python 另附自己的许可文本。
+- `licenses/isapi/README.txt` 只有本地读取到的版权/介绍文本,不能把它当作完整的独立许可授权。所有这些文件均按原样附上。
+- `pywin32` 的锁文件条件为 Windows;本清单没有据此推断它存在于 Vercel Linux 部署。
+
+此外,`httptools` 同时附带 http-parser、llhttp 的许可;`markdown-it-py` 附带 markdown-it 的许可;`cryptography` 与仅开发的 `packaging` 各自附有双许可原文。它们均以原始相对路径标识在汇总文本中保留,不替用户选择双许可分支。
+
+## 数据来源条款:独立于代码依赖许可
+
+[PROVIDER-TERMS.md](PROVIDER-TERMS.md) 记录了 2026-09-16 已读取的 Frankfurter、ECB、Federal Reserve Board、EIA 官方条款及其适用边界。这些声明没有转让上游数据所有权,也不能用 Python 依赖许可代替。保存来源链接、观测日期和对衍生计算的说明,并继续明确标注合成人物、价格及政策/能源样例。
+
+带密钥的 EIA 实时调用尚未完成;公开验证中的能源调用使用合成样例。Vercel Hobby 的计划资格及配额属于托管服务条款,不属于代码或数据许可。第一方许可和参赛授权由提交包中的 `RIGHTS.md` 单独载明;本清单不代签或变更该声明。
+
+## 原文副本核验
+
+已将 **68 个**实际存在的许可/通知文件汇总至 [THIRD_PARTY_NOTICES.txt](THIRD_PARTY_NOTICES.txt)。其中原文合计 **188,951 字节**,不含新增的索引标题和分隔符;每份嵌入原文均与原文件逐字节比对一致。表中原相对路径可直接在汇总文本中搜索,原始来源为已检查测试环境的 `site-packages/` 下对应 `dist-info` 位置。以下 SHA-256 校验的是每份原文,不是新增标题或整个汇总文件:
+
+| 汇总文本内的原相对路径标签 | 原文 SHA-256 |
+|---|---|
+| `dependency-notices/annotated_doc-0.0.5.dist-info/licenses/LICENSE` | `fff170779a6acbf65abdb405c087f1cee1786691e4a96a4034517e4a504a0cdf` |
+| `dependency-notices/annotated_types-0.8.0.dist-info/licenses/LICENSE` | `fe1049884b1a0d9342901e88e07f32925d24b3121d9972b6a6805fb9824b095d` |
+| `dependency-notices/anyio-4.15.1.dist-info/licenses/LICENSE` | `5361ac9dc58f2ef5fd2e9b09c68297c17f04950909bbc8023bdb82eacf22c2b0` |
+| `dependency-notices/attrs-26.1.0.dist-info/licenses/LICENSE` | `882115c95dfc2af1eeb6714f8ec6d5cbcabf667caff8729f42420da63f714e9f` |
+| `dependency-notices/certifi-2026.7.22.dist-info/licenses/LICENSE` | `e93716da6b9c0d5a4a1df60fe695b370f0695603d21f6f83f053e42cfc10caf7` |
+| `dependency-notices/cffi-2.1.1.dist-info/licenses/LICENSE` | `5ba24ddc57067f9249add644c3afc41a5d6dc37e23433ef759d95df370b0af63` |
+| `dependency-notices/click-8.5.0.dist-info/licenses/LICENSE.txt` | `9a8ad106a394e853bfe21f42f4e72d592819a22805d991b5f3275029292b658d` |
+| `dependency-notices/colorama-0.4.6.dist-info/licenses/LICENSE.txt` | `cac35c02686e5d04a5a7140bfb3b36e73aed496656e891102e428886d7930318` |
+| `dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE` | `3e0c7c091a948b82533ba98fd7cbb40432d6f1a9acbf85f5922d2f99a93ae6bb` |
+| `dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE` | `aac73b3148f6d1d7111dbca32099f68d26c644c6813ae1e4f05f6579aa2663fe` |
+| `dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD` | `602c4c7482de6479dd2e9793cda275e5e63d773dacd1eca689232ab7008fb4fb` |
+| `dependency-notices/fastapi-0.141.1.dist-info/licenses/LICENSE` | `4ec89ffc81485b97fec584b2d4a961032eeffe834453894fd9c1274906cc744e` |
+| `dependency-notices/h11-0.16.0.dist-info/licenses/LICENSE.txt` | `37db5bb85926db28a427a25867f10b1232003aea1be69ccb851138adb8e6f361` |
+| `dependency-notices/httpcore-1.0.9.dist-info/licenses/LICENSE.md` | `fdcb59154c74cbaba16a11242f7740bea9f23d6feb5547917d8c5f94a80392a5` |
+| `dependency-notices/httpcore2-2.12.0.dist-info/licenses/LICENSE.md` | `c4df125c807b0613501e943a5c92496dd11d2c16604ce396097a8dde396304d9` |
+| `dependency-notices/httptools-0.8.0.dist-info/licenses/LICENSE` | `1c028b059d7a58e14ccda38cabced698c591fe614ffbe4f721430446e86c8d69` |
+| `dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/http-parser/LICENSE-MIT` | `5cd1b682c0ccbac0cb31b86a9a1104c3e2e7b1377a282b8169d2694b400db7c2` |
+| `dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/llhttp/LICENSE` | `279012e02a10acfd59a3f2d8f13a497332535d871c2b27c89988985b06a3a438` |
+| `dependency-notices/httpx-0.28.1.dist-info/licenses/LICENSE.md` | `4ec59d544f12b5f539a3a716fd321ac58ccd8030b465221f2c880200cdf28d8d` |
+| `dependency-notices/httpx2-2.12.0.dist-info/licenses/LICENSE.md` | `7e7d6dbaf7fe160d39fd6e84fd02c33cab96bcbea34d36998d545bceb4a19c30` |
+| `dependency-notices/idna-3.19.dist-info/licenses/LICENSE.md` | `1a9a4f0e3d479a27240ddd59a9137a66ab4a0f9dfdc8ca6188cc0bfd85187f04` |
+| `dependency-notices/jsonschema-4.26.0.dist-info/licenses/COPYING` | `4f92a015a13c4d1a040bef018aa13430b4f1bc73b41b16bb846c346766de7439` |
+| `dependency-notices/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING` | `42dcd63495f87b4eb7c7757afa379bb55a53f94afd7a5f657d9adf57236e515c` |
+| `dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE` | `4a2260d6e2cd0f5a151a1e86dbfe7d3ed552b1e2beabf9941c1ba5c49cbce484` |
+| `dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE.markdown-it` | `792c48c5a849a15fdf9e37e8bcf9e6d1dd13b32b46c642a748a0a46a9919d473` |
+| `dependency-notices/mcp-2.2.0.dist-info/licenses/LICENSE` | `5e13dbbc1d120fc2a03cecde7c91424ae2d7de11b63d58ded2f4431e261ee50d` |
+| `dependency-notices/mcp_types-2.2.0.dist-info/licenses/LICENSE` | `5e13dbbc1d120fc2a03cecde7c91424ae2d7de11b63d58ded2f4431e261ee50d` |
+| `dependency-notices/mdurl-0.1.2.dist-info/LICENSE` | `7c605df6e28667a9603118e98274f64a49ce3eed0d26fccce9534a345e0ef955` |
+| `dependency-notices/opentelemetry_api-1.44.0.dist-info/licenses/LICENSE` | `c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4` |
+| `dependency-notices/pycparser-3.0.dist-info/licenses/LICENSE` | `0c846399369ea76ddd7b5c44fe6d16497415fcf015f5cbb508c24bf98b81c5b1` |
+| `dependency-notices/pydantic-2.13.5.dist-info/licenses/LICENSE` | `a9e186f3ca16b5eef84318e7a701721351a00cb7b8ae3a4394b67b49e3529ef3` |
+| `dependency-notices/pydantic_core-2.46.5.dist-info/licenses/LICENSE` | `fbe7f615f18d135c07000e5a86325db27f42c7729699ef166ac1f7a3f4586bf4` |
+| `dependency-notices/pygments-2.21.0.dist-info/licenses/AUTHORS` | `71a83872ad82f57be692cf827e048e2cd51d45169cfa1ea3f28706d97a872c0f` |
+| `dependency-notices/pygments-2.21.0.dist-info/licenses/LICENSE` | `a9d66f1d526df02e29dce73436d34e56e8632f46c275bbdffc70569e882f9f17` |
+| `dependency-notices/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst` | `925ce43461029eedbf558ec0b7cf7fc4b045def5120ebb97937c70814071cf07` |
+| `dependency-notices/pyjwt-2.14.0.dist-info/licenses/LICENSE` | `797a7a20231d4c433e9f1911db1731d06b5828b98f499819a034f7c0f56f5ce5` |
+| `dependency-notices/python_dotenv-1.2.3.dist-info/licenses/LICENSE` | `80619b7049f08c81683ad0e01f08f257a840652dd71ee83146d36658c7d2c2b9` |
+| `dependency-notices/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt` | `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` |
+| `dependency-notices/pywin32-312.dist-info/licenses/adodbapi/license.txt` | `5ea2f23c7f00c3006bacf267183e816d8d4b6dc95ea57a4ddb0ff81de6d8719e` |
+| `dependency-notices/pywin32-312.dist-info/licenses/com/License.txt` | `fad39c18fc3825fa278db1c86fd52cf88d7c525b3c2fbf7817fa383cbae4d423` |
+| `dependency-notices/pywin32-312.dist-info/licenses/com/win32comext/mapi/src/MAPIStubLibrary/LICENSE` | `1c4cf44d2fb8bf2d10214d6da80cc9e05502824cc4460f35be4c7bcb4cde53da` |
+| `dependency-notices/pywin32-312.dist-info/licenses/isapi/README.txt` | `41383bcc95edb8138d37077ae54bfdcbd2a3b77bf745a8529a9c161146e22303` |
+| `dependency-notices/pywin32-312.dist-info/licenses/pythonwin/License.txt` | `a74d826efb242db087b850348e20abbfd591b51e2081f2048735290913872924` |
+| `dependency-notices/pywin32-312.dist-info/licenses/pythonwin/pywin/idle/LICENSE.txt` | `a0fa1e7b8a4230055685e796548ac084a6c0eea395294574093bba72efd9191b` |
+| `dependency-notices/pywin32-312.dist-info/licenses/pythonwin/Scintilla/License.txt` | `57ec79bbeab4fd1cf7460a7ad450d2ab01471810459aeae9e75e4c66b472095f` |
+| `dependency-notices/pywin32-312.dist-info/licenses/win32/License.txt` | `a74d826efb242db087b850348e20abbfd591b51e2081f2048735290913872924` |
+| `dependency-notices/pyyaml-6.0.3.dist-info/licenses/LICENSE` | `8d3928f9dc4490fd635707cb88eb26bd764102a7282954307d3e5167a577e8a4` |
+| `dependency-notices/referencing-0.37.0.dist-info/licenses/COPYING` | `42dcd63495f87b4eb7c7757afa379bb55a53f94afd7a5f657d9adf57236e515c` |
+| `dependency-notices/rich-15.0.0.dist-info/licenses/LICENSE` | `deed7c17a4318158190a3ea239cc879a5a50271cebb98ae7025f48fbe58dca15` |
+| `dependency-notices/rpds_py-2026.6.3.dist-info/licenses/LICENSE` | `8bcb72c82ea8ae74802293c41d93ad7d51434001b0ae45a603a5af0f507aee0a` |
+| `dependency-notices/shellingham-1.5.4.dist-info/LICENSE` | `f388fd38cad13112c1dc0f669bbe80e7f84541edbafb72f3030d2ca7642c3c9d` |
+| `dependency-notices/sse_starlette-3.4.11.dist-info/licenses/AUTHORS` | `c2ff63d2ecf128bbda8ab1b2016830a5da124202fbb97168685bb9946aed3f1a` |
+| `dependency-notices/sse_starlette-3.4.11.dist-info/licenses/LICENSE` | `80af6bfccbebd2c14d4aab96695cab9950e177190cc80f5ef819e99996883e8e` |
+| `dependency-notices/starlette-1.6.0.dist-info/licenses/LICENSE.md` | `dcb95677a02240243187e964f941847d19b17821cf99e5afae684fab328c19bf` |
+| `dependency-notices/truststore-0.10.4.dist-info/licenses/LICENSE` | `33be7b7e8fa4fd19b1760e1a8ed8a668bdab852c91b692dd41424bcb725a9fca` |
+| `dependency-notices/typer-0.27.2.dist-info/licenses/LICENSE` | `58992cebcf8dfb6e40c4e2112ed12126c243666dca3912a3d78b7ecac4859d49` |
+| `dependency-notices/typing_extensions-4.16.0.dist-info/licenses/LICENSE` | `3b2f81fe21d181c499c59a256c8e1968455d6689d269aa85373bfb6af41da3bf` |
+| `dependency-notices/typing_inspection-0.4.4.dist-info/licenses/LICENSE` | `804b59b25f2c31bd278f9202a19ae49a3945aa2664387e2d0a128c7cacc61ec3` |
+| `dependency-notices/uvicorn-0.53.0.dist-info/licenses/LICENSE.md` | `efe1acf3e62fb99c288b0ec73e5a773b7268ef4320fe757ea994214e4b63c371` |
+| `dependency-notices/watchfiles-1.2.0.dist-info/licenses/LICENSE` | `2d40a03346bded8daa4bd22d23825a6f2568a0820fbef76ac05bae534421e189` |
+| `dependency-notices/websockets-17.1.dist-info/licenses/LICENSE` | `0f44514998aca209d3482d10204a8adf2aa4296ff157a36a5c0922f2280632d3` |
+| `dependency-notices/iniconfig-2.3.0.dist-info/licenses/LICENSE` | `3409fa91f7ace557894632676656e32264fe5ef7581535725dc9a23774551bd4` |
+| `dependency-notices/packaging-26.3.dist-info/licenses/LICENSE` | `cad1ef5bd340d73e074ba614d26f7deaca5c7940c3d8c34852e65c4909686c48` |
+| `dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.APACHE` | `0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594` |
+| `dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.BSD` | `b70e7e9b742f1cc6f948b34c16aa39ffece94196364bc88ff0d2180f0028fac5` |
+| `dependency-notices/pluggy-1.6.0.dist-info/licenses/LICENSE` | `d6b65e6c213a5d0b577911d34d6e5949b9f59d76c238c5071a2f3fc16cfb2606` |
+| `dependency-notices/pytest-9.1.1.dist-info/licenses/LICENSE` | `ca836a5f9ecca3b2f350230faa20a48fb8b145653b5568d784862df864706b9b` |
+| `dependency-notices/pytest_asyncio-1.4.0.dist-info/licenses/LICENSE` | `a8ad31b1c3f40dca5a84119351b8fa8ddc868edd77fad8a8ebf6d8f2d16fa4ae` |
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/PROVIDER-TERMS.md b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/PROVIDER-TERMS.md
new file mode 100644
index 0000000..9a55c9d
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/PROVIDER-TERMS.md
@@ -0,0 +1,25 @@
+# Third-party provider terms and supplemental dependency licenses
+
+Primary sources read on 2026-09-16. These notes retain the upstream statements checked during submission preparation and explain their application to this artifact. They do not replace the participant's declaration or grant new rights over upstream materials. See the [dependency inventory](DEPENDENCIES.md) and the [complete local license and notice texts](THIRD_PARTY_NOTICES.txt).
+
+## Hosted data sources
+
+| Source | Upstream statement and artifact boundary |
+| --- | --- |
+| Frankfurter | Its documentation permits free API use, including commercial use, with underlying data subject to each provider's terms. Requests are subject to anti-abuse rate limiting. This project uses the dedicated ECB route and identifies the provider; it does not copy Frankfurter server code. [Documentation](https://frankfurter.dev/) |
+| ECB statistics | The reuse policy permits free reuse of public statistics with source attribution and preservation of the statistics and metadata. Third-party data are excluded from that general permission. Preserve captured observations and their source dates; separately label project-derived cross-rate comparisons and impact calculations. [Statistics policy](https://www.ecb.europa.eu/stats/ecb_statistics/governance_and_quality_framework/html/usage_policy.de.html) |
+| ECB website and RSS | The copyright notice requires accurate reproduction and source attribution, and explicit disclosure of modifications. Named-author publications have separate restrictions. This artifact uses headline triggers and structured observations, not complete research papers. [Copyright notice](https://www.ecb.europa.eu/services/using-our-site/disclaimer/html/index.en.html) |
+| Federal Reserve Board RSS | Board information is generally public domain unless indicated otherwise; cite the Board. Third-party material and official seals/logos have separate restrictions. Retain headline source links and do not imply endorsement. [Disclaimer](https://www.federalreserve.gov/disclaimer.htm) |
+| EIA | EIA permits reuse of government information products with source acknowledgement including publication date. Third-party material, photos and marks have exceptions. EIA Open Data also points users to API terms. The current hosted verification used a synthetic energy fixture; it did not perform keyed EIA calls or imply permission for EIA logos. [Copyright and reuse](https://www.eia.gov/about/copyrights_reuse.php), [Open Data](https://www.eia.gov/opendata/) |
+
+The current evidence retains source URLs, observation timestamps and calculation traces. Its personal profile is explicitly synthetic, and the separately fetched headline is not presented as the cause of the FX change. Provider terms remain applicable when reusing the archived artifact; no ownership over upstream data is claimed.
+
+## Supplement to local dependency inventory
+
+The checked local test environment did not contain these installed distributions or an exact build-backend version. Their unavailable local metadata is supplemented by the following primary published sources, already read on the date above:
+
+- **uvloop 0.22.1:** the version's project page states dual MIT/Apache-2.0 licensing. [PyPI release](https://pypi.org/project/uvloop/0.22.1/)
+- **httpx2-jsfetch 1.0:** the version's project page reports BSD-3-Clause. [PyPI release](https://pypi.org/project/httpx2-jsfetch/1.0/)
+- **Hatch/Hatchling:** the upstream repository's current license is MIT. This does not establish the exact isolated build-backend version used by Vercel; the project build requirement is not version-pinned in the inspected lock. [Upstream license](https://github.com/pypa/hatch/blob/master/LICENSE.txt)
+
+The first two observations resolve the missing declared-license names, but are not claims that those packages were installed in the local Windows environment. This artifact does not include copied dependency implementations. [THIRD_PARTY_NOTICES.txt](THIRD_PARTY_NOTICES.txt) preserves all 68 original local license/notice texts that were actually available, under their original relative path labels, with SHA-256 and exact byte lengths. No uvloop, httpx2-jsfetch or Hatch license text has been invented or added to that local-text collection.
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/THIRD_PARTY_NOTICES.txt b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/THIRD_PARTY_NOTICES.txt
new file mode 100644
index 0000000..a582d8b
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/licenses/THIRD_PARTY_NOTICES.txt
@@ -0,0 +1,4119 @@
+THIRD-PARTY LICENSE AND NOTICE TEXTS
+Compiled 2026-09-16 from the checked test environment.
+Each Content-Length gives the exact byte count of the original text below it.
+Original texts are preserved byte for byte; surrounding headers are inventory labels.
+No dependency implementation source is included.
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/annotated_doc-0.0.5.dist-info/licenses/LICENSE
+SHA256: fff170779a6acbf65abdb405c087f1cee1786691e4a96a4034517e4a504a0cdf
+Content-Length: 1086
+
+The MIT License (MIT)
+
+Copyright (c) 2025 Sebastián Ramírez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/annotated_types-0.8.0.dist-info/licenses/LICENSE
+SHA256: fe1049884b1a0d9342901e88e07f32925d24b3121d9972b6a6805fb9824b095d
+Content-Length: 1083
+
+The MIT License (MIT)
+
+Copyright (c) 2022 the contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/anyio-4.15.1.dist-info/licenses/LICENSE
+SHA256: 5361ac9dc58f2ef5fd2e9b09c68297c17f04950909bbc8023bdb82eacf22c2b0
+Content-Length: 1081
+
+The MIT License (MIT)
+
+Copyright (c) 2018 Alex Grönholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/attrs-26.1.0.dist-info/licenses/LICENSE
+SHA256: 882115c95dfc2af1eeb6714f8ec6d5cbcabf667caff8729f42420da63f714e9f
+Content-Length: 1109
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Hynek Schlawack and the attrs contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/certifi-2026.7.22.dist-info/licenses/LICENSE
+SHA256: e93716da6b9c0d5a4a1df60fe695b370f0695603d21f6f83f053e42cfc10caf7
+Content-Length: 989
+
+This package contains a modified version of ca-bundle.crt:
+
+ca-bundle.crt -- Bundle of CA Root Certificates
+
+This is a bundle of X.509 certificates of public Certificate Authorities
+(CA). These were automatically extracted from Mozilla's root certificates
+file (certdata.txt). This file can be found in the mozilla source tree:
+https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt
+It contains the certificates in PEM format and therefore
+can be directly used with curl / libcurl / php_curl, or with
+an Apache+mod_ssl webserver for SSL client authentication.
+Just configure this file as the SSLCACertificateFile.#
+
+***** BEGIN LICENSE BLOCK *****
+This Source Code Form is subject to the terms of the Mozilla Public License,
+v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
+one at http://mozilla.org/MPL/2.0/.
+
+***** END LICENSE BLOCK *****
+@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/cffi-2.1.1.dist-info/licenses/LICENSE
+SHA256: 5ba24ddc57067f9249add644c3afc41a5d6dc37e23433ef759d95df370b0af63
+Content-Length: 1123
+
+
+Except when otherwise stated (look for LICENSE files in directories or
+information at the beginning of each file) all software and
+documentation is licensed as follows:
+
+ MIT No Attribution
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/click-8.5.0.dist-info/licenses/LICENSE.txt
+SHA256: 9a8ad106a394e853bfe21f42f4e72d592819a22805d991b5f3275029292b658d
+Content-Length: 1475
+
+Copyright 2014 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/colorama-0.4.6.dist-info/licenses/LICENSE.txt
+SHA256: cac35c02686e5d04a5a7140bfb3b36e73aed496656e891102e428886d7930318
+Content-Length: 1491
+
+Copyright (c) 2010 Jonathan Hartley
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holders, nor those of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE
+SHA256: 3e0c7c091a948b82533ba98fd7cbb40432d6f1a9acbf85f5922d2f99a93ae6bb
+Content-Length: 197
+
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made
+under the terms of *both* these licenses.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE
+SHA256: aac73b3148f6d1d7111dbca32099f68d26c644c6813ae1e4f05f6579aa2663fe
+Content-Length: 11360
+
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD
+SHA256: 602c4c7482de6479dd2e9793cda275e5e63d773dacd1eca689232ab7008fb4fb
+Content-Length: 1532
+
+Copyright (c) Individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of PyCA Cryptography nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/fastapi-0.141.1.dist-info/licenses/LICENSE
+SHA256: 4ec89ffc81485b97fec584b2d4a961032eeffe834453894fd9c1274906cc744e
+Content-Length: 1086
+
+The MIT License (MIT)
+
+Copyright (c) 2018 Sebastián Ramírez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/h11-0.16.0.dist-info/licenses/LICENSE.txt
+SHA256: 37db5bb85926db28a427a25867f10b1232003aea1be69ccb851138adb8e6f361
+Content-Length: 1124
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Nathaniel J. Smith and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httpcore-1.0.9.dist-info/licenses/LICENSE.md
+SHA256: fdcb59154c74cbaba16a11242f7740bea9f23d6feb5547917d8c5f94a80392a5
+Content-Length: 1518
+
+Copyright © 2020, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httpcore2-2.12.0.dist-info/licenses/LICENSE.md
+SHA256: c4df125c807b0613501e943a5c92496dd11d2c16604ce396097a8dde396304d9
+Content-Length: 1599
+
+Copyright © 2026 to present Pydantic Services Inc. and individual contributors.
+Copyright © 2020, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httptools-0.8.0.dist-info/licenses/LICENSE
+SHA256: 1c028b059d7a58e14ccda38cabced698c591fe614ffbe4f721430446e86c8d69
+Content-Length: 1114
+
+The MIT License
+
+Copyright (c) 2015 MagicStack Inc. http://magic.io
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/http-parser/LICENSE-MIT
+SHA256: 5cd1b682c0ccbac0cb31b86a9a1104c3e2e7b1377a282b8169d2694b400db7c2
+Content-Length: 1096
+
+Copyright Joyent, Inc. and other Node contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httptools-0.8.0.dist-info/licenses/vendor/llhttp/LICENSE
+SHA256: 279012e02a10acfd59a3f2d8f13a497332535d871c2b27c89988985b06a3a438
+Content-Length: 1091
+
+MIT License
+
+Copyright © 2018 Fedor Indutny
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httpx-0.28.1.dist-info/licenses/LICENSE.md
+SHA256: 4ec59d544f12b5f539a3a716fd321ac58ccd8030b465221f2c880200cdf28d8d
+Content-Length: 1508
+
+Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/httpx2-2.12.0.dist-info/licenses/LICENSE.md
+SHA256: 7e7d6dbaf7fe160d39fd6e84fd02c33cab96bcbea34d36998d545bceb4a19c30
+Content-Length: 1589
+
+Copyright © 2026 to present Pydantic Services Inc. and individual contributors.
+Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/idna-3.19.dist-info/licenses/LICENSE.md
+SHA256: 1a9a4f0e3d479a27240ddd59a9137a66ab4a0f9dfdc8ca6188cc0bfd85187f04
+Content-Length: 1541
+
+BSD 3-Clause License
+
+Copyright (c) 2013-2026, Kim Davies and contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/iniconfig-2.3.0.dist-info/licenses/LICENSE
+SHA256: 3409fa91f7ace557894632676656e32264fe5ef7581535725dc9a23774551bd4
+Content-Length: 1098
+
+The MIT License (MIT)
+
+Copyright (c) 2010 - 2023 Holger Krekel and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/jsonschema-4.26.0.dist-info/licenses/COPYING
+SHA256: 4f92a015a13c4d1a040bef018aa13430b4f1bc73b41b16bb846c346766de7439
+Content-Length: 1057
+
+Copyright (c) 2013 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING
+SHA256: 42dcd63495f87b4eb7c7757afa379bb55a53f94afd7a5f657d9adf57236e515c
+Content-Length: 1057
+
+Copyright (c) 2022 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE
+SHA256: 4a2260d6e2cd0f5a151a1e86dbfe7d3ed552b1e2beabf9941c1ba5c49cbce484
+Content-Length: 1078
+
+MIT License
+
+Copyright (c) 2020 ExecutableBookProject
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/markdown_it_py-4.2.0.dist-info/licenses/LICENSE.markdown-it
+SHA256: 792c48c5a849a15fdf9e37e8bcf9e6d1dd13b32b46c642a748a0a46a9919d473
+Content-Length: 1073
+
+Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/mcp-2.2.0.dist-info/licenses/LICENSE
+SHA256: 5e13dbbc1d120fc2a03cecde7c91424ae2d7de11b63d58ded2f4431e261ee50d
+Content-Length: 1071
+
+MIT License
+
+Copyright (c) 2024 Anthropic, PBC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/mcp_types-2.2.0.dist-info/licenses/LICENSE
+SHA256: 5e13dbbc1d120fc2a03cecde7c91424ae2d7de11b63d58ded2f4431e261ee50d
+Content-Length: 1071
+
+MIT License
+
+Copyright (c) 2024 Anthropic, PBC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/mdurl-0.1.2.dist-info/LICENSE
+SHA256: 7c605df6e28667a9603118e98274f64a49ce3eed0d26fccce9534a345e0ef955
+Content-Length: 2338
+
+Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
+Copyright (c) 2021 Taneli Hukkinen
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+--------------------------------------------------------------------------------
+
+.parse() is based on Joyent's node.js `url` code:
+
+Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/opentelemetry_api-1.44.0.dist-info/licenses/LICENSE
+SHA256: c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4
+Content-Length: 11357
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/packaging-26.3.dist-info/licenses/LICENSE
+SHA256: cad1ef5bd340d73e074ba614d26f7deaca5c7940c3d8c34852e65c4909686c48
+Content-Length: 197
+
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
+under the terms of *both* these licenses.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.APACHE
+SHA256: 0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594
+Content-Length: 10174
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/packaging-26.3.dist-info/licenses/LICENSE.BSD
+SHA256: b70e7e9b742f1cc6f948b34c16aa39ffece94196364bc88ff0d2180f0028fac5
+Content-Length: 1344
+
+Copyright (c) Donald Stufft and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pluggy-1.6.0.dist-info/licenses/LICENSE
+SHA256: d6b65e6c213a5d0b577911d34d6e5949b9f59d76c238c5071a2f3fc16cfb2606
+Content-Length: 1110
+
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pycparser-3.0.dist-info/licenses/LICENSE
+SHA256: 0c846399369ea76ddd7b5c44fe6d16497415fcf015f5cbb508c24bf98b81c5b1
+Content-Length: 1543
+
+pycparser -- A C parser in Python
+
+Copyright (c) 2008-2022, Eli Bendersky
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of the copyright holder nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pydantic-2.13.5.dist-info/licenses/LICENSE
+SHA256: a9e186f3ca16b5eef84318e7a701721351a00cb7b8ae3a4394b67b49e3529ef3
+Content-Length: 1129
+
+The MIT License (MIT)
+
+Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pydantic_core-2.46.5.dist-info/licenses/LICENSE
+SHA256: fbe7f615f18d135c07000e5a86325db27f42c7729699ef166ac1f7a3f4586bf4
+Content-Length: 1101
+
+The MIT License (MIT)
+
+Copyright (c) 2022 Samuel Colvin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pygments-2.21.0.dist-info/licenses/AUTHORS
+SHA256: 71a83872ad82f57be692cf827e048e2cd51d45169cfa1ea3f28706d97a872c0f
+Content-Length: 11048
+
+Pygments is written and maintained by Georg Brandl .
+
+Major developers are Tim Hatch and Armin Ronacher
+.
+
+Other contributors, listed alphabetically, are:
+
+* Sam Aaron -- Ioke lexer
+* Jean Abou Samra -- LilyPond lexer
+* João Abecasis -- JSLT lexer
+* Ali Afshar -- image formatter
+* Thomas Aglassinger -- Easytrieve, JCL, Rexx, Transact-SQL and VBScript
+ lexers
+* Maxence Ahlouche -- PostgreSQL Explain lexer
+* Muthiah Annamalai -- Ezhil lexer
+* Nikolay Antipov -- OpenSCAD lexer
+* Kumar Appaiah -- Debian control lexer
+* Andreas Amann -- AppleScript lexer
+* Timothy Armstrong -- Dart lexer fixes
+* Jeffrey Arnold -- R/S, Rd, BUGS, Jags, and Stan lexers
+* Eiríkr Åsheim -- Uxntal lexer
+* Jeremy Ashkenas -- CoffeeScript lexer
+* José Joaquín Atria -- Praat lexer
+* Stefan Matthias Aust -- Smalltalk lexer
+* Lucas Bajolet -- Nit lexer
+* Ben Bangert -- Mako lexers
+* Max Battcher -- Darcs patch lexer
+* Thomas Baruchel -- APL lexer
+* Tim Baumann -- (Literate) Agda lexer
+* Paul Baumgart, 280 North, Inc. -- Objective-J lexer
+* Michael Bayer -- Myghty lexers
+* Thomas Beale -- Archetype lexers
+* John Benediktsson -- Factor lexer
+* David Benjamin, Google LLC -- TLS lexer
+* Trevor Bergeron -- mIRC formatter
+* Vincent Bernat -- LessCSS lexer
+* Christopher Bertels -- Fancy lexer
+* Sébastien Bigaret -- QVT Operational lexer
+* Jarrett Billingsley -- MiniD lexer
+* Adam Blinkinsop -- Haskell, Redcode lexers
+* Stéphane Blondon -- Procfile, SGF and Sieve lexers
+* Frits van Bommel -- assembler lexers
+* Pierre Bourdon -- bugfixes
+* Martijn Braam -- Kernel log lexer, BARE lexer
+* JD Browne, Google LLC -- GoogleSQL lexer
+* Matthias Bussonnier -- ANSI style handling for terminal-256 formatter
+* chebee7i -- Python traceback lexer improvements
+* Hiram Chirino -- Scaml and Jade lexers
+* Mauricio Caceres -- SAS and Stata lexers.
+* Michael Camilleri, John Gabriele, sogaiu -- Janet lexer
+* Daren Chandisingh -- Gleam lexer
+* Ian Cooper -- VGL lexer
+* David Corbett -- Inform, Jasmin, JSGF, Snowball, and TADS 3 lexers
+* Leaf Corcoran -- MoonScript lexer
+* Fraser Cormack -- TableGen lexer
+* Gabriel Corona -- ASN.1 lexer
+* Radu Costache -- Boogie and Civl Verifier lexer improvements
+* Christopher Creutzig -- MuPAD lexer
+* Daniël W. Crompton -- Pike lexer
+* Pete Curry -- bugfixes
+* Bryan Davis -- EBNF lexer
+* Bruno Deferrari -- Shen lexer
+* Walter Dörwald -- UL4 lexer
+* Luke Drummond -- Meson lexer
+* Giedrius Dubinskas -- HTML formatter improvements
+* Owen Durni -- Haxe lexer
+* Alexander Dutton, Oxford University Computing Services -- SPARQL lexer
+* James Edwards -- Terraform lexer
+* Nick Efford -- Python 3 lexer
+* Sven Efftinge -- Xtend lexer
+* Artem Egorkine -- terminal256 formatter
+* Matthew Fernandez -- CAmkES lexer
+* Paweł Fertyk -- GDScript lexer, HTML formatter improvements
+* Michael Ficarra -- CPSA lexer
+* James H. Fisher -- PostScript lexer
+* Amanda Fitch, Google LLC -- GoogleSQL lexer
+* William S. Fulton -- SWIG lexer
+* Carlos Galdino -- Elixir and Elixir Console lexers
+* Michael Galloy -- IDL lexer
+* Naveen Garg -- Autohotkey lexer
+* Simon Garnotel -- FreeFem++ lexer
+* Laurent Gautier -- R/S lexer
+* Alex Gaynor -- PyPy log lexer
+* Richard Gerkin -- Igor Pro lexer
+* Alain Gilbert -- TypeScript lexer
+* Alex Gilding -- BlitzBasic lexer
+* GitHub, Inc -- DASM16, Augeas, TOML, and Slash lexers
+* Bertrand Goetzmann -- Groovy lexer
+* Krzysiek Goj -- Scala lexer
+* Rostyslav Golda -- FloScript lexer
+* Andrey Golovizin -- BibTeX lexers
+* Matt Good -- Genshi, Cheetah lexers
+* Michał Górny -- vim modeline support
+* Alex Gosse -- TrafficScript lexer
+* Patrick Gotthardt -- PHP namespaces support
+* Hubert Gruniaux -- C and C++ lexer improvements
+* Olivier Guibe -- Asymptote lexer
+* Armin Günther -- Go lexer improvements
+* Phil Hagelberg -- Fennel lexer
+* Florian Hahn -- Boogie lexer
+* Martin Harriman -- SNOBOL lexer
+* Matthew Harrison -- SVG formatter
+* Steven Hazel -- Tcl lexer
+* Dan Michael Heggø -- Turtle lexer
+* Aslak Hellesøy -- Gherkin lexer
+* Greg Hendershott -- Racket lexer
+* Justin Hendrick -- ParaSail lexer
+* Jordi Gutiérrez Hermoso -- Octave lexer
+* David Hess, Fish Software, Inc. -- Objective-J lexer
+* Ken Hilton -- Typographic Number Theory and Arrow lexers
+* Varun Hiremath -- Debian control lexer
+* Rob Hoelz -- Perl 6 lexer
+* Doug Hogan -- Mscgen lexer
+* Ben Hollis -- Mason lexer
+* Max Horn -- GAP lexer
+* Fred Hornsey -- OMG IDL Lexer
+* Alastair Houghton -- Lexer inheritance facility
+* Tim Howard -- BlitzMax lexer
+* Dustin Howett -- Logos lexer
+* Ivan Inozemtsev -- Fantom lexer
+* Hiroaki Itoh -- Shell console rewrite, Lexers for PowerShell session,
+ MSDOS session, BC, WDiff
+* Brian R. Jackson -- Tea lexer
+* Alex Jeffery, ChromaWay AB -- Rell lexer
+* Christian Jann -- ShellSession lexer
+* Jonas Camillus Jeppesen -- Line numbers and line highlighting for
+ RTF-formatter
+* Dennis Kaarsemaker -- sources.list lexer
+* Dmitri Kabak -- Inferno Limbo lexer
+* Igor Kalnitsky -- vhdl lexer
+* Colin Kennedy - USD lexer
+* Alexander Kit -- MaskJS lexer
+* Pekka Klärck -- Robot Framework lexer
+* Gerwin Klein -- Isabelle lexer
+* Eric Knibbe -- Lasso lexer
+* Stepan Koltsov -- Clay lexer
+* Oliver Kopp - Friendly grayscale style
+* Adam Koprowski -- Opa lexer
+* Benjamin Kowarsch -- Modula-2 lexer
+* Domen Kožar -- Nix lexer
+* Oleh Krekel -- Emacs Lisp lexer
+* Alexander Kriegisch -- Kconfig and AspectJ lexers
+* Marek Kubica -- Scheme lexer
+* Jochen Kupperschmidt -- Markdown processor
+* Gerd Kurzbach -- Modelica lexer
+* Jon Larimer, Google Inc. -- Smali lexer
+* Olov Lassus -- Dart lexer
+* Matt Layman -- TAP lexer
+* Dan Lazin, Google LLC -- GoogleSQL lexer
+* Kristian Lyngstøl -- Varnish lexers
+* Sylvestre Ledru -- Scilab lexer
+* Chee Sing Lee -- Flatline lexer
+* Mark Lee -- Vala lexer
+* Thomas Linder Puls -- Visual Prolog lexer
+* Pete Lomax -- Phix lexer
+* Valentin Lorentz -- C++ lexer improvements
+* Ben Mabey -- Gherkin lexer
+* Angus MacArthur -- QML lexer
+* Louis Mandel -- X10 lexer
+* Louis Marchand -- Eiffel lexer
+* Simone Margaritelli -- Hybris lexer
+* Tim Martin - World of Warcraft TOC lexer
+* Kirk McDonald -- D lexer
+* Gordon McGregor -- SystemVerilog lexer
+* Stephen McKamey -- Duel/JBST lexer
+* Brian McKenna -- F# lexer
+* Charles McLaughlin -- Puppet lexer
+* Kurt McKee -- Tera Term macro lexer, PostgreSQL updates, MySQL overhaul, JSON lexer
+* Joe Eli McIlvain -- Savi lexer
+* Lukas Meuser -- BBCode formatter, Lua lexer
+* Bruno Mesquita -- Night Owl style
+* Cat Miller -- Pig lexer
+* Paul Miller -- LiveScript lexer
+* Hong Minhee -- HTTP lexer
+* Michael Mior -- Awk lexer
+* Bruce Mitchener -- Dylan lexer rewrite
+* Reuben Morais -- SourcePawn lexer
+* Jon Morton -- Rust lexer
+* Paulo Moura -- Logtalk lexer
+* Mher Movsisyan -- DTD lexer
+* Dejan Muhamedagic -- Crmsh lexer
+* Adrien Nayrat -- PostgreSQL Explain lexer
+* Ana Nelson -- Ragel, ANTLR, R console lexers
+* David Neto, Google LLC -- WebGPU Shading Language lexer
+* Kurt Neufeld -- Markdown lexer
+* Nam T. Nguyen -- Monokai style
+* Jesper Noehr -- HTML formatter "anchorlinenos"
+* Mike Nolta -- Julia lexer
+* Avery Nortonsmith -- Pointless lexer
+* Jonas Obrist -- BBCode lexer
+* Edward O'Callaghan -- Cryptol lexer
+* David Oliva -- Rebol lexer
+* Pat Pannuto -- nesC lexer
+* Jon Parise -- Protocol buffers and Thrift lexers
+* Benjamin Peterson -- Test suite refactoring
+* Ronny Pfannschmidt -- BBCode lexer
+* Dominik Picheta -- Nimrod lexer
+* Andrew Pinkham -- RTF Formatter Refactoring
+* Clément Prévost -- UrbiScript lexer
+* Tanner Prynn -- cmdline -x option and loading lexers from files
+* Oleh Prypin -- Crystal lexer (based on Ruby lexer)
+* Nick Psaris -- K and Q lexers
+* Xidorn Quan -- Web IDL lexer
+* Elias Rabel -- Fortran fixed form lexer
+* raichoo -- Idris lexer
+* Daniel Ramirez -- GDScript lexer
+* Kashif Rasul -- CUDA lexer
+* Nathan Reed -- HLSL lexer
+* Justin Reidy -- MXML lexer
+* Jonathon Reinhart, Google LLC -- Soong lexer
+* Norman Richards -- JSON lexer
+* Corey Richardson -- Rust lexer updates
+* Fabrizio Riguzzi -- cplint leder
+* Lubomir Rintel -- GoodData MAQL and CL lexers
+* Andre Roberge -- Tango style
+* Georg Rollinger -- HSAIL lexer
+* Michiel Roos -- TypoScript lexer
+* Konrad Rudolph -- LaTeX formatter enhancements
+* Mario Ruggier -- Evoque lexers
+* Miikka Salminen -- Lovelace style, Hexdump lexer, lexer enhancements
+* Stou Sandalski -- NumPy, FORTRAN, tcsh and XSLT lexers
+* Matteo Sasso -- Common Lisp lexer
+* Joe Schafer -- Ada lexer
+* Max Schillinger -- TiddlyWiki5 lexer
+* Andrew Schmidt -- X++ lexer
+* Ken Schutte -- Matlab lexers
+* René Schwaiger -- Rainbow Dash style
+* Sebastian Schweizer -- Whiley lexer
+* Tassilo Schweyer -- Io, MOOCode lexers
+* Pablo Seminario -- PromQL lexer
+* Ted Shaw -- AutoIt lexer
+* Joerg Sieker -- ABAP lexer
+* Robert Simmons -- Standard ML lexer
+* Kirill Simonov -- YAML lexer
+* Corbin Simpson -- Monte lexer
+* Ville Skyttä -- ASCII armored lexer
+* Alexander Smishlajev -- Visual FoxPro lexer
+* Steve Spigarelli -- XQuery lexer
+* Jerome St-Louis -- eC lexer
+* Camil Staps -- Clean and NuSMV lexers; Solarized style
+* James Strachan -- Kotlin lexer
+* Tom Stuart -- Treetop lexer
+* Colin Sullivan -- SuperCollider lexer
+* Ben Swift -- Extempore lexer
+* tatt61880 -- Kuin lexer
+* Edoardo Tenani -- Arduino lexer
+* Tiberius Teng -- default style overhaul
+* Jeremy Thurgood -- Erlang, Squid config lexers
+* Brian Tiffin -- OpenCOBOL lexer
+* Bob Tolbert -- Hy lexer
+* Doug Torrance -- Macaulay2 lexer
+* Matthias Trute -- Forth lexer
+* Tuoa Spi T4 -- Bdd lexer
+* Erick Tryzelaar -- Felix lexer
+* Alexander Udalov -- Kotlin lexer improvements
+* Thomas Van Doren -- Chapel lexer
+* Dave Van Ee -- Uxntal lexer updates
+* Daniele Varrazzo -- PostgreSQL lexers
+* Abe Voelker -- OpenEdge ABL lexer
+* Pepijn de Vos -- HTML formatter CTags support
+* Matthias Vallentin -- Bro lexer
+* Benoît Vinot -- AMPL lexer
+* Linh Vu Hong -- RSL lexer
+* Taavi Väänänen -- Debian control, PHP lexers
+* Immanuel Washington -- Smithy lexer
+* Nathan Weizenbaum -- Haml and Sass lexers
+* Nathan Whetsell -- Csound lexers
+* Dietmar Winkler -- Modelica lexer
+* Nils Winter -- Smalltalk lexer
+* Trevor Woerner -- BitBake lexer
+* Davy Wybiral -- Clojure lexer
+* Whitney Young -- ObjectiveC lexer
+* Diego Zamboni -- CFengine3 lexer
+* Enrique Zamudio -- Ceylon lexer
+* Alex Zimin -- Nemerle lexer
+* Rob Zimmerman -- Kal lexer
+* Evgenii Zheltonozhskii -- Maple lexer
+* Vincent Zurczak -- Roboconf lexer
+* Hubert Gruniaux -- C and C++ lexer improvements
+* Thomas Symalla -- AMDGPU Lexer
+* 15b3 -- Image Formatter improvements
+* Fabian Neumann -- CDDL lexer
+* Thomas Duboucher -- CDDL lexer
+* Philipp Imhof -- Pango Markup formatter
+* Thomas Voss -- Sed lexer
+* Martin Fischer -- WCAG contrast testing
+* Marc Auberer -- Spice lexer
+* Amr Hesham -- Carbon lexer
+* diskdance -- Wikitext lexer
+* vanillajonathan -- PRQL lexer
+* Nikolay Antipov -- OpenSCAD lexer
+* Markus Meyer, Nextron Systems -- YARA lexer
+* Hannes Römer -- Mojo lexer
+* Jan Frederik Schaefer -- PDDL lexer
+
+Many thanks for all contributions!
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pygments-2.21.0.dist-info/licenses/LICENSE
+SHA256: a9d66f1d526df02e29dce73436d34e56e8632f46c275bbdffc70569e882f9f17
+Content-Length: 1331
+
+Copyright (c) 2006-2022 by the respective authors (see AUTHORS file).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst
+SHA256: 925ce43461029eedbf558ec0b7cf7fc4b045def5120ebb97937c70814071cf07
+Content-Length: 322
+
+Authors
+=======
+
+``pyjwt`` is currently written and maintained by `Jose Padilla `_.
+Originally written and maintained by `Jeff Lindsay `_.
+
+A full list of contributors can be found on GitHub’s `overview `_.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pyjwt-2.14.0.dist-info/licenses/LICENSE
+SHA256: 797a7a20231d4c433e9f1911db1731d06b5828b98f499819a034f7c0f56f5ce5
+Content-Length: 1085
+
+The MIT License (MIT)
+
+Copyright (c) 2015-2022 José Padilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pytest-9.1.1.dist-info/licenses/LICENSE
+SHA256: ca836a5f9ecca3b2f350230faa20a48fb8b145653b5568d784862df864706b9b
+Content-Length: 1091
+
+The MIT License (MIT)
+
+Copyright (c) 2004 Holger Krekel and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pytest_asyncio-1.4.0.dist-info/licenses/LICENSE
+SHA256: a8ad31b1c3f40dca5a84119351b8fa8ddc868edd77fad8a8ebf6d8f2d16fa4ae
+Content-Length: 11324
+
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/python_dotenv-1.2.3.dist-info/licenses/LICENSE
+SHA256: 80619b7049f08c81683ad0e01f08f257a840652dd71ee83146d36658c7d2c2b9
+Content-Length: 1556
+
+Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv)
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+- Neither the name of django-dotenv nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt
+SHA256: cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30
+Content-Length: 11358
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/adodbapi/license.txt
+SHA256: 5ea2f23c7f00c3006bacf267183e816d8d4b6dc95ea57a4ddb0ff81de6d8719e
+Content-Length: 26925
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/com/License.txt
+SHA256: fad39c18fc3825fa278db1c86fd52cf88d7c525b3c2fbf7817fa383cbae4d423
+Content-Length: 1570
+
+Unless stated in the specific source file, this work is
+Copyright (c) 1996-2008, Greg Stein and Mark Hammond.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in
+the documentation and/or other materials provided with the distribution.
+
+Neither names of Greg Stein, Mark Hammond nor the name of contributors may be used
+to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
+IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/com/win32comext/mapi/src/MAPIStubLibrary/LICENSE
+SHA256: 1c4cf44d2fb8bf2d10214d6da80cc9e05502824cc4460f35be4c7bcb4cde53da
+Content-Length: 1087
+
+MIT License
+
+Copyright (c) 2018 Microsoft
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/isapi/README.txt
+SHA256: 41383bcc95edb8138d37077ae54bfdcbd2a3b77bf745a8529a9c161146e22303
+Content-Length: 330
+
+A Python ISAPI extension. Contributed by Phillip Frantz, and is
+Copyright 2002-2003 by Blackdog Software Pty Ltd.
+
+See the 'samples' directory, and particularly samples\README.txt
+
+You can find documentation in the PyWin32.chm file that comes with pywin32 -
+you can open this from Pythonwin->Help, or from the start menu.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/pythonwin/License.txt
+SHA256: a74d826efb242db087b850348e20abbfd591b51e2081f2048735290913872924
+Content-Length: 1541
+
+Unless stated in the specific source file, this work is
+Copyright (c) 1994-2008, Mark Hammond
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in
+the documentation and/or other materials provided with the distribution.
+
+Neither name of Mark Hammond nor the name of contributors may be used
+to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
+IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/pythonwin/pywin/idle/LICENSE.txt
+SHA256: a0fa1e7b8a4230055685e796548ac084a6c0eea395294574093bba72efd9191b
+Content-Length: 12540
+
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations (see
+http://www.digicool.com). In 2001, the Python Software Foundation
+(PSF, see http://www.python.org/psf/) was formed, a non-profit
+organization created specifically to own Python-related Intellectual
+Property. Digital Creations is a sponsoring member of the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI no
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.2 2.1.1 2001 PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PSF LICENSE AGREEMENT FOR PYTHON 2.2
+------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using Python 2.2 software in source or binary form and its
+associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 2.2
+alone or in any derivative version, provided, however, that PSF's
+License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
+2001 Python Software Foundation; All Rights Reserved" are retained in
+Python 2.2 alone or in any derivative version prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 2.2 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 2.2.
+
+4. PSF is making Python 2.2 available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.2 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+2.2 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.2,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python 2.2, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/pythonwin/Scintilla/License.txt
+SHA256: 57ec79bbeab4fd1cf7460a7ad450d2ab01471810459aeae9e75e4c66b472095f
+Content-Length: 873
+
+License for Scintilla and SciTE
+
+Copyright 1998-2003 by Neil Hodgson
+
+All Rights Reserved
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation.
+
+NEIL HODGSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS, IN NO EVENT SHALL NEIL HODGSON BE LIABLE FOR ANY
+SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
+OR PERFORMANCE OF THIS SOFTWARE.
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pywin32-312.dist-info/licenses/win32/License.txt
+SHA256: a74d826efb242db087b850348e20abbfd591b51e2081f2048735290913872924
+Content-Length: 1541
+
+Unless stated in the specific source file, this work is
+Copyright (c) 1994-2008, Mark Hammond
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in
+the documentation and/or other materials provided with the distribution.
+
+Neither name of Mark Hammond nor the name of contributors may be used
+to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
+IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/pyyaml-6.0.3.dist-info/licenses/LICENSE
+SHA256: 8d3928f9dc4490fd635707cb88eb26bd764102a7282954307d3e5167a577e8a4
+Content-Length: 1101
+
+Copyright (c) 2017-2021 Ingy döt Net
+Copyright (c) 2006-2016 Kirill Simonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/referencing-0.37.0.dist-info/licenses/COPYING
+SHA256: 42dcd63495f87b4eb7c7757afa379bb55a53f94afd7a5f657d9adf57236e515c
+Content-Length: 1057
+
+Copyright (c) 2022 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/rich-15.0.0.dist-info/licenses/LICENSE
+SHA256: deed7c17a4318158190a3ea239cc879a5a50271cebb98ae7025f48fbe58dca15
+Content-Length: 1056
+
+Copyright (c) 2020 Will McGugan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/rpds_py-2026.6.3.dist-info/licenses/LICENSE
+SHA256: 8bcb72c82ea8ae74802293c41d93ad7d51434001b0ae45a603a5af0f507aee0a
+Content-Length: 1076
+
+Copyright (c) 2023 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/shellingham-1.5.4.dist-info/LICENSE
+SHA256: f388fd38cad13112c1dc0f669bbe80e7f84541edbafb72f3030d2ca7642c3c9d
+Content-Length: 751
+
+Copyright (c) 2018, Tzu-ping Chung
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/sse_starlette-3.4.11.dist-info/licenses/AUTHORS
+SHA256: c2ff63d2ecf128bbda8ab1b2016830a5da124202fbb97168685bb9946aed3f1a
+Content-Length: 395
+
+sysid
+synodriver
+Skyler Lewis
+Margret `Pax` Williams
+nekonoshiri
+justindujardin
+Håvard Thom <34199185+havardthom@users.noreply.github.com>
+Vaibhav Mule
+issmirnov
+Cyprien
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/sse_starlette-3.4.11.dist-info/licenses/LICENSE
+SHA256: 80af6bfccbebd2c14d4aab96695cab9950e177190cc80f5ef819e99996883e8e
+Content-Length: 1511
+
+Copyright © 2020, [sysid](https://sysid.github.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/starlette-1.6.0.dist-info/licenses/LICENSE.md
+SHA256: dcb95677a02240243187e964f941847d19b17821cf99e5afae684fab328c19bf
+Content-Length: 1518
+
+Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/truststore-0.10.4.dist-info/licenses/LICENSE
+SHA256: 33be7b7e8fa4fd19b1760e1a8ed8a668bdab852c91b692dd41424bcb725a9fca
+Content-Length: 1086
+
+The MIT License (MIT)
+
+Copyright (c) 2022 Seth Michael Larson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/typer-0.27.2.dist-info/licenses/LICENSE
+SHA256: 58992cebcf8dfb6e40c4e2112ed12126c243666dca3912a3d78b7ecac4859d49
+Content-Length: 1086
+
+The MIT License (MIT)
+
+Copyright (c) 2019 Sebastián Ramírez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/typing_extensions-4.16.0.dist-info/licenses/LICENSE
+SHA256: 3b2f81fe21d181c499c59a256c8e1968455d6689d269aa85373bfb6af41da3bf
+Content-Length: 13936
+
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations, which became
+Zope Corporation. In 2001, the Python Software Foundation (PSF, see
+https://www.python.org/psf/) was formed, a non-profit organization
+created specifically to own Python-related Intellectual Property.
+Zope Corporation was a sponsoring member of the PSF.
+
+All Python releases are Open Source (see https://opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+Python software and documentation are licensed under the
+Python Software Foundation License Version 2.
+
+Starting with Python 3.8.6, examples, recipes, and other code in
+the documentation are dual licensed under the PSF License Version 2
+and the Zero-Clause BSD license.
+
+Some software incorporated into Python is under different licenses.
+The licenses are listed with code falling under that license.
+
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
+----------------------------------------------------------------------
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/typing_inspection-0.4.4.dist-info/licenses/LICENSE
+SHA256: 804b59b25f2c31bd278f9202a19ae49a3945aa2664387e2d0a128c7cacc61ec3
+Content-Length: 1090
+
+MIT License
+
+Copyright (c) Pydantic Services Inc. 2025 to present
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/uvicorn-0.53.0.dist-info/licenses/LICENSE.md
+SHA256: efe1acf3e62fb99c288b0ec73e5a773b7268ef4320fe757ea994214e4b63c371
+Content-Length: 1526
+
+Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/watchfiles-1.2.0.dist-info/licenses/LICENSE
+SHA256: 2d40a03346bded8daa4bd22d23825a6f2568a0820fbef76ac05bae534421e189
+Content-Length: 1112
+
+The MIT License (MIT)
+
+Copyright (c) 2017 to present Samuel Colvin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=== END NOTICE ===
+
+=== BEGIN NOTICE ===
+Path: dependency-notices/websockets-17.1.dist-info/licenses/LICENSE
+SHA256: 0f44514998aca209d3482d10204a8adf2aa4296ff157a36a5c0922f2280632d3
+Content-Length: 1538
+
+Copyright (c) Aymeric Augustin and contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=== END NOTICE ===
+
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/official-validation.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/official-validation.json
new file mode 100644
index 0000000..a4aa295
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/official-validation.json
@@ -0,0 +1,72 @@
+{
+ "recorded_at": "2026-09-17T07:03:38.451224+00:00",
+ "source_review_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "submission_directory": "submissions/mcp-hackathon/kongkoukk-life-exchange-rate",
+ "validator_file": "scripts/validate-submission.mjs",
+ "validator_sha256": "1023f229ed5a3e33b12cd7c7d5f072fe9d4a6461b49c831c7e31502e2829954f",
+ "tls_verification": "Official validator unchanged; no TLS override supplied",
+ "runs": [
+ {
+ "mode": "offline",
+ "started_at": "2026-09-17T07:03:38.451224+00:00",
+ "finished_at": "2026-09-17T07:03:38.956449+00:00",
+ "command": "npm run validate:submission -- --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate",
+ "exit_code": 0,
+ "stdout": "\n> @xagt/agent-plugin@0.5.0 validate:submission\n> node scripts/validate-submission.mjs --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate\n\n{\n \"status\": \"pass\",\n \"slug\": \"kongkoukk-life-exchange-rate\",\n \"checks\": [\n \"submission structure and manifest are valid\",\n \"73 source files are present; all submission files passed the baseline secret scan (not a completeness or security certification)\",\n \"verification evidence contains a health check and repeatable curl command\"\n ],\n \"manifest\": {\n \"schemaVersion\": 1,\n \"name\": \"Life Exchange Rate MCP\",\n \"slug\": \"kongkoukk-life-exchange-rate\",\n \"sourceRepository\": \"https://github.com/KongkouKK/life-exchange-rate-mcp\",\n \"reviewCommit\": \"238954196ea6922d3cea2ae8acb914b0f4e01dbb\",\n \"apiBaseUrl\": \"https://life-exchange-rate-mcp.vercel.app\",\n \"healthCheckUrl\": \"https://life-exchange-rate-mcp.vercel.app/health\",\n \"deploymentProofUrl\": \"https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json\"\n }\n}\n",
+ "stderr": "npm notice\nnpm notice New major version of npm available! 10.9.4 -> 12.0.2\nnpm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2\nnpm notice To update run: npm install -g npm@12.0.2\nnpm notice\n",
+ "validator_result": {
+ "status": "pass",
+ "slug": "kongkoukk-life-exchange-rate",
+ "checks": [
+ "submission structure and manifest are valid",
+ "73 source files are present; all submission files passed the baseline secret scan (not a completeness or security certification)",
+ "verification evidence contains a health check and repeatable curl command"
+ ],
+ "manifest": {
+ "schemaVersion": 1,
+ "name": "Life Exchange Rate MCP",
+ "slug": "kongkoukk-life-exchange-rate",
+ "sourceRepository": "https://github.com/KongkouKK/life-exchange-rate-mcp",
+ "reviewCommit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "healthCheckUrl": "https://life-exchange-rate-mcp.vercel.app/health",
+ "deploymentProofUrl": "https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json"
+ }
+ },
+ "status": "passed"
+ },
+ {
+ "mode": "online",
+ "started_at": "2026-09-17T07:03:38.956449+00:00",
+ "finished_at": "2026-09-17T07:03:42.721732+00:00",
+ "command": "npm run validate:submission -- --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate --online",
+ "exit_code": 0,
+ "stdout": "\n> @xagt/agent-plugin@0.5.0 validate:submission\n> node scripts/validate-submission.mjs --dir submissions/mcp-hackathon/kongkoukk-life-exchange-rate --online\n\n{\n \"status\": \"pass\",\n \"slug\": \"kongkoukk-life-exchange-rate\",\n \"checks\": [\n \"submission structure and manifest are valid\",\n \"73 source files are present; all submission files passed the baseline secret scan (not a completeness or security certification)\",\n \"verification evidence contains a health check and repeatable curl command\",\n \"the declared source version has been verified\",\n \"the live health check is reachable and bound to the review commit\",\n \"the standard deployment proof confirms control of the live service\"\n ],\n \"manifest\": {\n \"schemaVersion\": 1,\n \"name\": \"Life Exchange Rate MCP\",\n \"slug\": \"kongkoukk-life-exchange-rate\",\n \"sourceRepository\": \"https://github.com/KongkouKK/life-exchange-rate-mcp\",\n \"reviewCommit\": \"238954196ea6922d3cea2ae8acb914b0f4e01dbb\",\n \"apiBaseUrl\": \"https://life-exchange-rate-mcp.vercel.app\",\n \"healthCheckUrl\": \"https://life-exchange-rate-mcp.vercel.app/health\",\n \"deploymentProofUrl\": \"https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json\"\n }\n}\n",
+ "stderr": "npm notice\nnpm notice New major version of npm available! 10.9.4 -> 12.0.2\nnpm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2\nnpm notice To update run: npm install -g npm@12.0.2\nnpm notice\n",
+ "validator_result": {
+ "status": "pass",
+ "slug": "kongkoukk-life-exchange-rate",
+ "checks": [
+ "submission structure and manifest are valid",
+ "73 source files are present; all submission files passed the baseline secret scan (not a completeness or security certification)",
+ "verification evidence contains a health check and repeatable curl command",
+ "the declared source version has been verified",
+ "the live health check is reachable and bound to the review commit",
+ "the standard deployment proof confirms control of the live service"
+ ],
+ "manifest": {
+ "schemaVersion": 1,
+ "name": "Life Exchange Rate MCP",
+ "slug": "kongkoukk-life-exchange-rate",
+ "sourceRepository": "https://github.com/KongkouKK/life-exchange-rate-mcp",
+ "reviewCommit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "apiBaseUrl": "https://life-exchange-rate-mcp.vercel.app",
+ "healthCheckUrl": "https://life-exchange-rate-mcp.vercel.app/health",
+ "deploymentProofUrl": "https://life-exchange-rate-mcp.vercel.app/.well-known/xagent-verification.json"
+ }
+ },
+ "status": "passed"
+ }
+ ],
+ "status": "passed"
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-files.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-files.json
new file mode 100644
index 0000000..8441d27
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-files.json
@@ -0,0 +1,304 @@
+{
+ "reviewCommit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "sourceRepository": "https://github.com/KongkouKK/life-exchange-rate-mcp",
+ "note": "File hashes of the supplied review source. Its relationship to the public commit requires separate verification.",
+ "excluded_submission_boilerplate": [
+ "RIGHTS.md",
+ "SUBMISSION.md",
+ "submission.json"
+ ],
+ "files": [
+ {
+ "path": ".env.example",
+ "sha256": "39cca14cb50bf69820d9de781457abd73fbd23ea98451fcdfc6fd8f62860d672"
+ },
+ {
+ "path": ".gitignore",
+ "sha256": "a8249d12043716a27e88ca8c49fa3452c8d1add323bc5a43f2ff081eee3b3322"
+ },
+ {
+ "path": ".python-version",
+ "sha256": "8dfda54ba2f66ab3a8ebab460f4338bbeea1fb1e0f85ce06fe3530aa3c64679b"
+ },
+ {
+ "path": ".vercelignore",
+ "sha256": "9f6a705e80b7c26890c81b7f09d00ab3fe18509f6b7d7bd6ee084b9a558cf3dc"
+ },
+ {
+ "path": "app.py",
+ "sha256": "2af46c91540b85d4fa0a931e9fbf59919cf866912e76cfd587db38f3b97fea18"
+ },
+ {
+ "path": "CHANGELOG.md",
+ "sha256": "aa12a027e8a86bdb92f3ecd17a298a2df0903a4299ac66a6009ff2af54072905"
+ },
+ {
+ "path": "DEPLOYMENT_STATUS.md",
+ "sha256": "5e1db9176f3fde23d6a388293106fb19184d81fbd5a5bab1a4c139199312420c"
+ },
+ {
+ "path": "Dockerfile",
+ "sha256": "b474bad6fda59d621fe19ab6973886721a0b1a012775cc2b8428226b3cb9ed7c"
+ },
+ {
+ "path": "docs/ARCHITECTURE.md",
+ "sha256": "35a9f341536c4a2cebb70234da3ea35ee81cd5b03e802bf71850755621bce689"
+ },
+ {
+ "path": "docs/DATA_SOURCES.md",
+ "sha256": "3f985acb0661667ee79ff113c1d83fe875c21afc6e7318bc7b559570b05a679e"
+ },
+ {
+ "path": "docs/DEMO.md",
+ "sha256": "87e6914f57ebb4642f016a02827e4afe28bae976738100ba23eb84c742c14301"
+ },
+ {
+ "path": "docs/DEPLOY_FREE.md",
+ "sha256": "08799331f359059eedb5c482ca3bb85be8edbd1285892c21b6bdfb6a0b73aa25"
+ },
+ {
+ "path": "docs/EVAL_CASES.md",
+ "sha256": "9b41c6d8a83d4f316a8da3040f19121b55351bc56afd7599a3d9b276528b3275"
+ },
+ {
+ "path": "docs/FX_ANOMALY.md",
+ "sha256": "73d059a831f616ea07498131cc71f0121b0b330d635e9ebecadc7cfe5d7181b2"
+ },
+ {
+ "path": "docs/JUDGE_DEMO.md",
+ "sha256": "7a028627424cc4d45e9081974dd5e77c941264cff422489c5de9560356d8e0cc"
+ },
+ {
+ "path": "docs/PRODUCT_SPEC.md",
+ "sha256": "25e3a23bdbe53e3b4e5cd070f55ed80104e7eb71bdeb904ef9c836a2289bf966"
+ },
+ {
+ "path": "docs/ROADMAP_18_DAYS.md",
+ "sha256": "c3ac8c840a1651bcf8ff51909dc421a29ca5e7342d441c3a774c18ebb2d59e7e"
+ },
+ {
+ "path": "docs/TOOL_CONTRACTS.md",
+ "sha256": "1cb7d6649ada1e407188836ec8d404c071295353e701c35dabdaee489a569e4d"
+ },
+ {
+ "path": "pyproject.toml",
+ "sha256": "e4af6dfd032b691c4a78b48425dde5d992c2cef838319f418efa00cf736f6f24"
+ },
+ {
+ "path": "README.md",
+ "sha256": "eec85ef810e680a1e0ced055c27b07682ecc3d98a661f494893dafe09f93f4f4"
+ },
+ {
+ "path": "source/life_exchange_rate/__init__.py",
+ "sha256": "667d4a15b970b851e20d17510224670c14646cfb6d5a1e388ca6b9cc6da8bf41"
+ },
+ {
+ "path": "source/life_exchange_rate/calculations.py",
+ "sha256": "bb73a5d31514c379b656ef2920baae196327f7db8606a0f2fde2976633761aef"
+ },
+ {
+ "path": "source/life_exchange_rate/demo.py",
+ "sha256": "4bc60f5cb1efdb7ed263cb25dd9936f7dd0e1997da9ee8c91c93ecc8a7ab1cda"
+ },
+ {
+ "path": "source/life_exchange_rate/main.py",
+ "sha256": "7132116492357f6d370e286953a02c204b6026413ec57f103f7e24691e8d2abe"
+ },
+ {
+ "path": "source/life_exchange_rate/mcp_server.py",
+ "sha256": "1ec8b983cc363140cb5aa6e4501e1d0b19e8809c1406f8242dee6b70689873f7"
+ },
+ {
+ "path": "source/life_exchange_rate/models.py",
+ "sha256": "c67e15c3d622af25b94b604931ae4e85bf731859783551920542138bf6a859ac"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/common.py",
+ "sha256": "7066cdbe44b8bbb9aaf562dc63c9f6a2a145f7be59db04bf5792b11e2ad48af3"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/data/energy.json",
+ "sha256": "b0c4ebf1ac7c3b661b7987e029439b4dcffb2489cc2d4c98e2e70c2326d2606b"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/data/policy_rate.json",
+ "sha256": "b8e6c347ffe47f70dbd590ab2f08424e3f5d17269a375087107198aaee543511"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/energy.py",
+ "sha256": "aaee25bf1a5dca847e888ac9ddf644358cf3ecf522201e7bf1e7dbfb4a4fbd4b"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/fixtures.py",
+ "sha256": "6f7e6c95f65d6b6e89b9e5e946ff107db395482c1d6502efbee6b1063d10a391"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/frankfurter.py",
+ "sha256": "f99257d815d55230521c3ae51f984633e2a59bc2771975b3113844f2c99a94f0"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/fx_anomaly.py",
+ "sha256": "b63f9caf350c9c0f0f0eeb10c4792b399d58cbc5c4d6958dd77abd06a25e7cd3"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/official_rss.py",
+ "sha256": "dabb8389118c788cf1cb11c8e1e6c6999ad2a0ef17d8ed447336f1db32ca2520"
+ },
+ {
+ "path": "source/life_exchange_rate/providers/policy_rate.py",
+ "sha256": "ded286ed2f68f4bc48f21c9d6c9b693c4afca6c359b9fa8b784d7039e3b7fb04"
+ },
+ {
+ "path": "source/life_exchange_rate/radar.py",
+ "sha256": "ee1cf84e05f625a38a6f965a08300f9fac7f9f6a5492c0d5e377e2cc11ec29dc"
+ },
+ {
+ "path": "source/life_exchange_rate/service.py",
+ "sha256": "e8a2b0311be7b696bb60ef88d9baad9cc84f697e30f007106a93b3648beb23ac"
+ },
+ {
+ "path": "tests/test_api.py",
+ "sha256": "3ccc5948c0f059e42dea490dc6b3985b4c3a3429c92c9ad6dd96396a3b633338"
+ },
+ {
+ "path": "tests/test_calculations.py",
+ "sha256": "255dd8d1df1c9aafe96555b1d09d63054bf443f46453a987c06e707fab174d49"
+ },
+ {
+ "path": "tests/test_deployment_config.py",
+ "sha256": "275023cd6f3eeeea3bbec4b3434cae7d7694415b221e76b5effaac08774a4456"
+ },
+ {
+ "path": "tests/test_frankfurter.py",
+ "sha256": "afc4fba3f3844f60e0266b8db061e0c55ebd1b32f3270ec90591a0eb37e94612"
+ },
+ {
+ "path": "tests/test_guardrails.py",
+ "sha256": "f4fe0ae282888d034fc02144146a955ff1404b8ead11f3549f2a9864e527b6e6"
+ },
+ {
+ "path": "tests/test_mcp.py",
+ "sha256": "31f1058c129ae2023e07d225817c791ebc2f26a2b371f1967121c27af04708b1"
+ },
+ {
+ "path": "tests/test_official_rss.py",
+ "sha256": "02fcab88ab7578a2e26b211b126800fcaf89eda98cb00f91c591f7cefd5619ac"
+ },
+ {
+ "path": "tests/test_radar.py",
+ "sha256": "7002ca37e234fc919b0139b71ea5bb7740bfdcdc285fe1c901abc8e938e6a01c"
+ },
+ {
+ "path": "tests/test_structured_providers.py",
+ "sha256": "998026978d8416507a4bfa9d40d344fd26f6a5a2f257390a54f4a45415a7c9a1"
+ },
+ {
+ "path": "tests/test_verification.py",
+ "sha256": "8d10559f60b5aa09973915ccaed31fbcc4207995f443378976078d73cdc14007"
+ },
+ {
+ "path": "uv.lock",
+ "sha256": "d67c058ded87d2778474f63fe051214025122c77982f80baf1e7bee1c94a4e5d"
+ },
+ {
+ "path": "vercel.json",
+ "sha256": "bc0cec871f652284058677a7fd7a3518b05b5bd2ba6eeee128a66b55646fa22e"
+ },
+ {
+ "path": "verification/build-check.json",
+ "sha256": "3f0646bb12b69d088fc08ed78ac206b098dc073c048cd57fff043a66d04b44de"
+ },
+ {
+ "path": "verification/end-to-end-demo.json",
+ "sha256": "148303410e8cebf6a3b2354722ec4b629d1d4bca6f0b1c5cbfbfbdda46d4d6a9"
+ },
+ {
+ "path": "verification/fixtures/energy-event.json",
+ "sha256": "a25c40f58eab8835913d3dbce9d48b9718b2a6e6a7bc887b5d513b2d91fdeb84"
+ },
+ {
+ "path": "verification/fixtures/energy-request.json",
+ "sha256": "134e03109c662b1458b85001a806c68481b8c2ea923386c662fac82d1e21941d"
+ },
+ {
+ "path": "verification/fixtures/energy-response.json",
+ "sha256": "fc43c9b4d87f04813fd38ba11a2c684f15973c00931f7b7bb1d2b2930a979a14"
+ },
+ {
+ "path": "verification/fixtures/fx-event.json",
+ "sha256": "6a428967bd9986ef91228b4199edb14d0a52e44516bae6b63d79e050fdb503fd"
+ },
+ {
+ "path": "verification/fixtures/fx-request.json",
+ "sha256": "6cbdb20183fbdfae4dc0e087f8bb456f03f437c4e8fc96f107d958310a70e628"
+ },
+ {
+ "path": "verification/fixtures/fx-response.json",
+ "sha256": "836da27b4e43d11918188189258105d7b9b0ff99f4f16e62f950a9c0679e132e"
+ },
+ {
+ "path": "verification/fixtures/invalid-request.json",
+ "sha256": "e556521df3d9b56d94dc4532927d04ce3e02294a4e65bb49953310c7995e9779"
+ },
+ {
+ "path": "verification/fixtures/offline-demo.json",
+ "sha256": "18daa0c974edc0c7f30f2e05e7adbfc63b1676e06139065f1cb5a3d36d14aa1f"
+ },
+ {
+ "path": "verification/fixtures/policy-event.json",
+ "sha256": "bf6470b7c60b3d565096a3c96902f372c5378596a1b3b1385cb99866c17c15cc"
+ },
+ {
+ "path": "verification/fixtures/policy-request.json",
+ "sha256": "d64898c30b747c5f35c903f770db7760cbb61a1e55a9d24ed3fe9e110a46ac6f"
+ },
+ {
+ "path": "verification/fixtures/policy-response.json",
+ "sha256": "537293ff74bc11680896a178b6bff060b58183dbcdf12ab056f578c99448f60d"
+ },
+ {
+ "path": "verification/generate_fixtures.py",
+ "sha256": "ece289c566a195d581cee63c419e3d503d41b6a91fd557f1d69821f111a4d0c6"
+ },
+ {
+ "path": "verification/hosted-live-2026-09-16.json",
+ "sha256": "659960c6b20246e8d979bfd9b8514d91f43e75e7678d518800381d40101d4ec6"
+ },
+ {
+ "path": "verification/live-cli-demo.json",
+ "sha256": "ba19bdd3cb0485419fd561362c6341cefdad256d3ac16a8200c27d894c16fdf0"
+ },
+ {
+ "path": "verification/live-providers.json",
+ "sha256": "1df981c0219356938509c944ca05e752190940a6c33dc1746ad39b3308593d84"
+ },
+ {
+ "path": "verification/mcp-smoke.json",
+ "sha256": "582783f0188e9e14043e3f1e6e8f89fd0c99b7dfeba9b7d82474e68b4cfcbf32"
+ },
+ {
+ "path": "verification/MCP_SMOKE.md",
+ "sha256": "4a31060c9a555540e52e99d7b223afc58d87ae75a25218cfc189c6807bb0ae67"
+ },
+ {
+ "path": "verification/openapi.json",
+ "sha256": "51904365e407c45f62010b3fae2c1016c6383f16347cde6f394097c02ef79d73"
+ },
+ {
+ "path": "verification/README.md",
+ "sha256": "fe55ba8f0e1cb750d8eba403f82df78b00bf141f9d3e264050a1d1f9bbe5ae46"
+ },
+ {
+ "path": "verification/record_live_providers.py",
+ "sha256": "f7849c4e06a6de71a3c1e963bbe2111f7b8bd245d3a247400e6cb1b210847857"
+ },
+ {
+ "path": "verification/smoke_mcp.py",
+ "sha256": "bc6d4dbf4367f5ea6bb0380efb62c8980dfbc397006b111cde6d574f469c9b4d"
+ },
+ {
+ "path": "verification/summary.json",
+ "sha256": "30867f50854be781d57e0b1e77627302f267653ba041a6602f7d85baddaf0695"
+ }
+ ]
+}
diff --git a/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-verification-final.json b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-verification-final.json
new file mode 100644
index 0000000..5b9143c
--- /dev/null
+++ b/submissions/mcp-hackathon/kongkoukk-life-exchange-rate/verification/source-verification-final.json
@@ -0,0 +1,20 @@
+{
+ "recorded_at": "2026-09-16T09:54:47.366796+00:00",
+ "source_repository": "https://github.com/KongkouKK/life-exchange-rate-mcp",
+ "public_commit": "238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "archive_url": "https://codeload.github.com/KongkouKK/life-exchange-rate-mcp/zip/238954196ea6922d3cea2ae8acb914b0f4e01dbb",
+ "archive_sha256": "c75a6210c5ff1ca6a277f2d10acb1e99dc0b797eb36294d930f2820a136dc79f",
+ "comparison": "Exact UTF-8 bytes except CRLF/CR line endings; binary bytes exact",
+ "local_file_count": 76,
+ "remote_file_count": 76,
+ "line_ending_only_paths": [
+ "tests/test_deployment_config.py"
+ ],
+ "missing_paths": [],
+ "differing_paths": [],
+ "remote_only_paths": [],
+ "local_source_matches_remote": true,
+ "file_sets_match": true,
+ "runtime_complete_and_matches_local": true,
+ "runtime_unchanged_since_148_test_pass": true
+}