diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 5fcb249b..c26e6640 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -13,7 +13,7 @@
"name": "agentops-accelerator",
"source": "../../plugins/agentops",
"description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.",
- "version": "0.4.1",
+ "version": "0.6.0",
"keywords": [
"agentops",
"evaluation",
diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json
index 5fcb249b..c26e6640 100644
--- a/.github/plugin/marketplace.json
+++ b/.github/plugin/marketplace.json
@@ -13,7 +13,7 @@
"name": "agentops-accelerator",
"source": "../../plugins/agentops",
"description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.",
- "version": "0.4.1",
+ "version": "0.6.0",
"keywords": [
"agentops",
"evaluation",
diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml
index 80f5009d..508b17bc 100644
--- a/.github/workflows/_build.yml
+++ b/.github/workflows/_build.yml
@@ -39,7 +39,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
ref: ${{ inputs.checkout_ref || github.ref }}
fetch-depth: 0 # Full history required for setuptools-scm
diff --git a/.github/workflows/agentops-watchdog.yml b/.github/workflows/agentops-watchdog.yml
index f87eccd6..637f0a46 100644
--- a/.github/workflows/agentops-watchdog.yml
+++ b/.github/workflows/agentops-watchdog.yml
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Azure login (OIDC)
uses: azure/login@v3
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f4529df3..d07e3a23 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -66,7 +66,7 @@ jobs:
os: [ubuntu-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13"]
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -125,7 +125,7 @@ jobs:
permissions:
id-token: write # Required for PyPI Trusted Publishing (OIDC)
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0 # Full history for setuptools-scm
@@ -162,7 +162,7 @@ jobs:
needs: publish-dev
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -215,7 +215,7 @@ jobs:
build-vsix:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
# CI uses the committed package.json version as-is (no publish, dry-run only).
# The version in package.json is synced by cut-release.yml when a release branch is created.
diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml
index 51d4d756..aa3d43da 100644
--- a/.github/workflows/cut-release.yml
+++ b/.github/workflows/cut-release.yml
@@ -48,7 +48,7 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_ENV"
- name: Checkout develop
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
ref: develop
fetch-depth: 0
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 7aefdab6..2c3c3062 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -36,7 +36,7 @@ jobs:
offline-smoke:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
@@ -69,7 +69,7 @@ jobs:
unit-tests-with-coverage:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
@@ -127,7 +127,7 @@ jobs:
hosted_agent_name: ${{ steps.create_hosted_agent.outputs.agent_name }}
suffix: ${{ steps.suffix.outputs.value }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- id: suffix
name: Compute per-run suffix
@@ -256,7 +256,7 @@ jobs:
id-token: write
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -307,7 +307,7 @@ jobs:
id-token: write
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -359,7 +359,7 @@ jobs:
id-token: write
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -409,7 +409,7 @@ jobs:
id-token: write
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -464,7 +464,7 @@ jobs:
id-token: write
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Azure login (OIDC)
uses: ./.github/actions/azure-oidc-login
with:
@@ -529,7 +529,7 @@ jobs:
if: always()
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 99d1ad9b..c45e49f7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -98,7 +98,7 @@ jobs:
needs: publish-testpypi
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -185,7 +185,7 @@ jobs:
env:
VSIX_FILE: agentops-skills.vsix
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0 # Full history for version derivation
@@ -255,7 +255,7 @@ jobs:
permissions:
contents: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Download Python dist artifacts
uses: actions/download-artifact@v8
diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml
index 59cf2d79..a39d2870 100644
--- a/.github/workflows/staging.yml
+++ b/.github/workflows/staging.yml
@@ -79,7 +79,7 @@ jobs:
needs: publish-testpypi
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -137,7 +137,7 @@ jobs:
runs-on: ubuntu-latest
environment: staging
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Sync VSIX version from branch name
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a75d13af..ac44a673 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,17 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres
## [Unreleased]
+## [0.6.0] - 2026-06-26
+
+### Added
+- **Retrieval telemetry can now be imported as evaluation datasets.** The new
+ `telemetry_imports` config contract and `agentops telemetry validate`,
+ `agentops telemetry preview`, and `agentops telemetry import` commands let
+ teams turn reviewed retrieval telemetry into dataset-backed eval rows with
+ `response_source: dataset`. Grey-box HTTP agents can map `response_fields` from
+ `$response.context`, and the evaluation docs now cover the import workflow and
+ contract.
+
### Changed
- **Prompt-agent PR validation now uses sandbox instead of dev.** Generated
GitHub and Azure DevOps PR workflows stage prompt-agent candidates in the
@@ -178,6 +189,25 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres
tutorial are updated to describe the new contract.
([#214](https://github.com/Azure/agentops/issues/214))
+### Fixed
+- **Clean installs now include the pager dependency used by explain commands.**
+ `agentops explain`, `agentops init explain`, and `agentops doctor explain`
+ import Click directly to render long manual output, so `click>=8.1,<9` is now
+ declared as a runtime dependency instead of relying on transitive installs.
+
+- **`agentops eval init` now works with both old and new `azure.ai.agents` azd
+ extensions.** Version 0.1.40 of the extension renamed the eval subcommand from
+ `azd ai agent eval init` to `azd ai agent eval generate`, which made
+ `agentops eval init` hard-fail with `Command "init" is deprecated, use 'azd ai
+ agent eval generate' instead`. AgentOps now invokes `generate` first and
+ transparently falls back to the legacy `init` subcommand when an older
+ extension does not recognise `generate`. The fallback only triggers on
+ subcommand-name/deprecation errors; genuine failures (authentication, project
+ endpoint, timeouts) are still surfaced immediately and unchanged. All
+ previously passed flags (`--project-endpoint`, `--agent`,
+ `--gen-instruction-file`, `--eval-model`, `--dataset`, `--evaluator`) and the
+ recipe discovery/persistence behaviour are preserved.
+
## [0.4.0] - 2026-06-14
### Added
diff --git a/README.md b/README.md
index 0264af95..20621044 100644
--- a/README.md
+++ b/README.md
@@ -55,12 +55,110 @@ practices.
## Learn more
For setup guides, tutorials, architecture, CI/CD guidance, Doctor checks, and
-evaluator reference, see:
+evaluator reference, start with the documentation site:
https://aka.ms/agentops-accelerator
+## Run a first evaluation
+
+```powershell
+az login
+$env:AZURE_AI_FOUNDRY_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
+$env:AZURE_OPENAI_ENDPOINT = "https://.openai.azure.com"
+$env:AZURE_OPENAI_DEPLOYMENT = "gpt-4o-mini"
+agentops eval analyze
+agentops eval run
+agentops doctor --evidence-pack
+```
+
+For Foundry targets, use either `project_endpoint:` in `agentops.yaml` or
+`AZURE_AI_FOUNDRY_PROJECT_ENDPOINT`. Config wins when both are set.
+
+Outputs land in `.agentops/results/latest/`:
+
+- `results.json` - machine-readable (versioned, stable schema)
+- `report.md` - human-readable, PR-friendly
+
+Release evidence lands in `.agentops/release/latest/`:
+
+- `evidence.json` - machine-readable production-readiness projection
+- `evidence.md` - PR/release summary
+
+Capture the first successful run as a baseline:
+
+```powershell
+New-Item -ItemType Directory -Force .agentops\baseline | Out-Null
+Copy-Item .agentops\results\latest\results.json .agentops\baseline\results.json
+```
+
+To see a visible comparison, publish a new agent version with a prompt
+that paraphrases instead of copying exact-answer requests, update
+`agentops.yaml` to that new `name:version`, and compare against the
+baseline:
+
+```powershell
+agentops eval run --baseline .agentops/baseline/results.json
+```
+
+The report grows a `Comparison vs Baseline` section with per-metric deltas.
+
+---
+
+## Commands
+
+Install optional extras as needed: `[agent]` for Doctor/Cockpit and `[mcp]` for MCP.
+
+- `agentops --version` - show installed version.
+- `agentops init` - bootstrap config and seed data.
+- `agentops eval analyze` - check eval readiness.
+- `agentops eval init` - bootstrap an azd `eval.yaml` recipe and wire `execution: azd`.
+- `agentops eval run [--baseline PATH]` - run an evaluation.
+- `agentops eval promote-traces --source FILE [--apply]` - promote local trace export files.
+- `agentops telemetry validate NAME` - validate an Azure Monitor or Application Insights import.
+- `agentops telemetry preview NAME --rows N` - preview telemetry import rows.
+- `agentops telemetry import NAME --apply` - write the imported telemetry dataset.
+- `agentops report generate` - regenerate `report.md`.
+- `agentops workflow analyze` - recommend CI/CD shape.
+- `agentops workflow generate` - generate CI/CD workflows.
+- `agentops skills install` - install Copilot or Claude skills.
+- `agentops mcp serve` - start the MCP server.
+- `agentops doctor [--evidence-pack]` - run readiness checks.
+- `agentops cockpit` - open the local Cockpit.
+- `agentops agent serve` - serve Doctor as a Copilot Extension.
+
+## AgentOps Cockpit
+
+`agentops cockpit` opens a localhost command center for the current workspace.
+It combines eval history, Doctor findings, workflow status, and links to the
+matching Foundry and Azure Monitor views.
+
+Cockpit sections, in display order:
+
+- **Foundry connection** - project, tenant, agent, App Insights.
+- **Foundry launchpad** - links for the agent, project, and telemetry.
+- **Observability readiness** - tracing, evals, red team, alerts.
+- **AgentOps Doctor** - latest Doctor findings.
+- **Eval gate summary** - local and CI gate history.
+- **Quality gate summary** - score trends and regressions.
+- **Production signal** - App Insights health snapshot.
+- **CI/CD Pipelines** - GitHub Actions status.
+- **Next actions** - contextual recommendations.
+
+## Documentation
+
+- [Foundry Prompt Agent tutorial](docs/tutorial-prompt-agent.md) - use this when the Foundry target is `agent: name:version`. Walks the sandbox to dev journey with a PR gate.
+- [Hosted or HTTP Agent tutorial](docs/tutorial-hosted-agent-quickstart.md) - use this when the target is a Foundry hosted or HTTP endpoint URL. Same sandbox to dev journey for endpoint-based agents.
+- [End-to-end tutorial](docs/tutorial-end-to-end.md) - extends either of the above with the full sandbox to dev to qa to prod promotion, Foundry red-team scans, and trace-to-regression promotion.
+- [Evaluation paths](docs/evaluation.md) - choose static dataset, grey-box HTTP, or telemetry/trace import.
+- [Core concepts](docs/concepts.md)
+- [How it works](docs/how-it-works.md)
+- [Doctor explained](docs/doctor-explained.md)
+- [CI/CD with GitHub Actions](docs/ci-github-actions.md)
+- [Built-in evaluator reference](docs/foundry-evaluation-sdk-built-in-evaluators.md)
+- [Release process](docs/release-process.md)
+
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for development, testing, and contribution guidance.
\ No newline at end of file
diff --git a/docs/concepts.md b/docs/concepts.md
index 93be6d3d..39ce4efa 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -96,8 +96,10 @@ Common `agent:` values:
| `"model:gpt-4o-mini"` | Direct model deployment |
HTTP targets can add top-level mapping fields such as `request_field`,
-`response_field`, `tool_calls_field`, `auth_header_env`, and
-`extra_fields`.
+`response_fields`, `tool_calls_field`, `auth_header_env`, and `extra_fields`.
+Use `response_fields.response` for the final answer and
+`response_fields.context` for retrieved context. Use `response_source: dataset`
+when each dataset row already contains the response to evaluate.
### Dataset
@@ -198,6 +200,8 @@ AgentOps auto-selects common evaluation patterns from the dataset:
Use one of the three hands-on tutorials for scenario coverage:
+- [Evaluation paths](evaluation.md) explains when to use a static dataset,
+ grey-box HTTP response mapping, or telemetry/trace import.
- [Foundry Prompt Agent tutorial](tutorial-prompt-agent-quickstart.md) for Foundry
prompt agents referenced as `name:version`.
- [Hosted or HTTP Agent tutorial](tutorial-hosted-agent-quickstart.md) for Foundry
@@ -215,9 +219,13 @@ the fields your target needs:
version: 1
agent: "https://api.example.com/chat"
dataset: .agentops/data/support.jsonl
+response_source: agent
+protocol: http-json
request_field: message
-response_field: text
+response_fields:
+ response: text
+ context: retrieved_context
thresholds:
coherence: ">=3"
diff --git a/docs/evaluation.md b/docs/evaluation.md
new file mode 100644
index 00000000..aedaee64
--- /dev/null
+++ b/docs/evaluation.md
@@ -0,0 +1,257 @@
+# Evaluation
+
+Use this page when you need to choose how AgentOps should evaluate a RAG or
+agent workflow. The goal is simple: pick the path that matches where your
+evidence comes from, run the evaluation, and keep the result in a format that
+reviewers can trust.
+
+AgentOps supports three evaluation paths:
+
+1. **Static dataset**: use a JSONL file that already contains the prompt,
+ expected answer, and optional retrieval context.
+2. **Grey-box HTTP**: call an HTTP endpoint and extract both the answer and
+ retrieval context from the live response.
+3. **Telemetry/trace import**: import production traces into a reviewable
+ dataset so real traffic can become future regression coverage.
+
+## Choose a path
+
+| Path | Use it when | Best first step |
+|---|---|---|
+| Static dataset | You already know the test cases, expected answers, and optionally the target responses. | Create or edit `.agentops/data/*.jsonl`. |
+| Grey-box HTTP | Your endpoint can return the answer plus retrieval details for the same request. | Configure `request_field` and `response_fields`. |
+| Telemetry/trace import | You want to learn from production traffic before adding new regression rows. | Configure `telemetry_imports`, then run `agentops telemetry preview`. |
+
+The paths build on each other. Most teams start with a static dataset, add
+grey-box HTTP when they need retrieval telemetry, then use telemetry import after
+the agent is running in production.
+
+```mermaid
+flowchart LR
+ Static[Static dataset] --> HTTP[Grey-box HTTP]
+ HTTP --> Traces[Telemetry import]
+ Traces --> Static
+```
+
+## Static dataset
+
+Choose this path when the data you need is already in the dataset file. Each row
+is a test case. AgentOps sends `input` to the target, compares the target
+response with `expected`, and uses `context` when present to select RAG
+evaluators.
+
+By default, `response_source: agent` means AgentOps calls the configured target.
+Use `response_source: dataset` only when the dataset already includes the answer
+you want to evaluate in a `response`, `prediction`, `output`, or `answer` field.
+That is useful for offline review or imported trace rows that should not call a
+live endpoint again.
+
+Minimal RAG row:
+
+```json
+{"id":"refund-001","input":"What is the refund window?","expected":"Customers can request a refund within 30 days.","context":"Refunds are available for 30 days after purchase."}
+```
+
+Minimal config:
+
+```yaml
+version: 1
+agent: "support-agent:3"
+dataset: .agentops/data/rag-smoke.jsonl
+response_source: agent
+
+thresholds:
+ groundedness: ">=3"
+ retrieval: ">=3"
+ response_completeness: ">=3"
+```
+
+Run it:
+
+```powershell
+agentops eval analyze
+agentops eval run
+```
+
+Use this path for:
+
+- Fast local checks before opening a PR.
+- CI gates with stable examples.
+- Baseline comparison with `agentops eval run --baseline`.
+- Manual review of newly written or newly labeled examples.
+
+## Grey-box HTTP
+
+Choose this path when the endpoint can return more than final text. This is the
+best path for RAG services because the evaluator can see what the agent actually
+retrieved for the request.
+
+The endpoint response should include:
+
+- the final answer;
+- retrieval context, citations, or document chunks;
+- optional tool calls or workflow metadata.
+
+Example endpoint response:
+
+```json
+{
+ "answer": "Customers can request a refund within 30 days.",
+ "context": [
+ "Refunds are available for 30 days after purchase.",
+ "Refunds require the original order number."
+ ],
+ "citations": ["refund-policy.md"]
+}
+```
+
+Example config:
+
+```yaml
+version: 1
+agent: "https://support-dev.example.com/chat"
+dataset: .agentops/data/rag-smoke.jsonl
+
+protocol: http-json
+request_field: message
+response_fields:
+ response: answer
+ context: context
+ citations: citations
+
+thresholds:
+ groundedness: ">=3"
+ retrieval: ">=3"
+ relevance: ">=3"
+```
+
+What happens:
+
+1. AgentOps reads each row from the dataset.
+2. It sends `row.input` as the HTTP request field named by `request_field`.
+3. It extracts the final answer from `response_fields.response`.
+4. It extracts retrieval context from `response_fields.context`.
+5. RAG evaluators can use the extracted context through `$response.context`,
+ `$retrieved_context`, or `$retrieved_context_items`.
+
+Use dot paths when fields are nested:
+
+```yaml
+response_fields:
+ response: output.text
+ context: output.retrieval.chunks
+```
+
+Use this path for:
+
+- RAG services where the retrieved chunks matter.
+- Debugging why a groundedness or retrieval score changed.
+- Endpoint-based agents hosted in Azure Container Apps, AKS, Foundry Hosted
+ Agents, or another HTTP host.
+
+## Telemetry import
+
+Choose this path when production traffic has useful examples that are not yet in
+your test set. Telemetry import does not make production responses automatically
+correct. It creates reviewable dataset candidates.
+
+Configure a named telemetry import in `agentops.yaml`:
+
+```yaml
+telemetry_imports:
+ - name: prod-rag
+ target: application-insights
+ resource_id: $APPINSIGHTS_RESOURCE_ID
+ time_range:
+ lookback_days: 7
+ filters:
+ customDimensions.agent: support-agent
+ fields:
+ input: customDimensions.question
+ response: customDimensions.answer
+ context: customDimensions.retrieved_context
+ trace_id: operation_Id
+ output:
+ path: .agentops/data/prod-rag-candidates.jsonl
+ label_mode: pending
+```
+
+Validate the import without querying Azure:
+
+```powershell
+agentops telemetry validate prod-rag
+```
+
+Preview rows from Azure Monitor:
+
+```powershell
+agentops telemetry preview prod-rag --rows 10
+```
+
+Write the candidate dataset and manifest:
+
+```powershell
+agentops telemetry import prod-rag --apply
+```
+
+Label modes:
+
+| Mode | What it writes | Use it when |
+|---|---|---|
+| `pending` | Empty `expected` values with review metadata. | A human must write the correct answer before the row can gate a release. |
+| `self-similarity` | The production response becomes `expected`. | You want drift detection against known production behavior. |
+
+Telemetry import keeps lineage metadata such as trace ID, timestamp, replay URL,
+and source system when those values exist in the export. If the trace includes
+retrieval context, AgentOps writes it as `context` so RAG evaluators can use it
+later. Evaluator mappings can also use `$telemetry.trace_id` when a trace ID is
+needed for reporting or troubleshooting.
+
+If you already have a local trace export file, `agentops eval promote-traces`
+still works. Use `agentops telemetry` when the source is Azure Monitor or
+Application Insights.
+
+Use this path for:
+
+- Turning incidents or surprising production answers into regression tests.
+- Sampling real traffic for future review.
+- Building a trace-to-dataset flywheel without skipping human judgment.
+
+## Input mapping
+
+Evaluator inputs come from three places:
+
+| Source | Placeholder | Example |
+|---|---|---|
+| Dataset prompt | `$row.input` or `$prompt` | User question sent to the agent. |
+| Dataset expected answer | `$row.expected` or `$expected` | Ground truth or acceptance criteria. |
+| Agent response | `$response.response` or `$prediction` | Final answer returned by the target. |
+| Any response field | `$response.` | Any field extracted through `response_fields`. |
+| Extracted retrieval context | `$response.context`, `$retrieved_context`, or `$retrieved_context_items` | Chunks, citations, or grounding text from the live response. |
+| Dataset retrieval context | `$row.context` | Static context stored in JSONL. |
+| Trace ID | `$telemetry.trace_id` | Azure Monitor or Application Insights operation ID. |
+
+For beginners, the easiest rule is:
+
+- Put known test data in the dataset.
+- Put live endpoint outputs under `response_fields`.
+- Let AgentOps map the common fields to evaluators.
+
+Only customize evaluator selection when the automatic choice is not enough:
+
+```yaml
+evaluators:
+ - GroundednessEvaluator
+ - RetrievalEvaluator
+ - RelevanceEvaluator
+```
+
+## Safety notes
+
+- Do not treat production responses as ground truth without review.
+- Do not import sensitive trace payloads into a repository dataset.
+- Keep secrets in environment variables or `.agentops/.env`, not in JSONL files.
+- Prefer `--label-mode pending` when correctness matters.
+- Use `self-similarity` only for drift detection.
+- Keep trace replay links in metadata so reviewers can investigate the original
+ runtime behavior.
diff --git a/docs/foundry-evaluation-sdk-built-in-evaluators.md b/docs/foundry-evaluation-sdk-built-in-evaluators.md
index 87cfe859..5fb77230 100644
--- a/docs/foundry-evaluation-sdk-built-in-evaluators.md
+++ b/docs/foundry-evaluation-sdk-built-in-evaluators.md
@@ -1,218 +1,210 @@
-# Foundry Evaluation SDK Built-in Evaluators (AgentOps)
+# Foundry Evaluators
-This guide maps Microsoft Foundry built-in evaluators to the configuration model used by AgentOps Toolkit.
+This page explains how AgentOps maps Microsoft Foundry Evaluation SDK
+evaluators to the data in `agentops.yaml`, dataset rows, HTTP responses, and
+trace imports.
-## 1) AgentOps config shape (quick reference)
+Most users do not need to configure evaluator internals. AgentOps selects common
+evaluators from the target type and dataset shape. Use this page when you need
+to understand what each evaluator receives.
-In AgentOps, each evaluator is configured under `bundle.evaluators[]`:
+## Config shape
+
+The normal config stays small:
```yaml
-evaluators:
- - name: SimilarityEvaluator
- source: foundry
- enabled: true
- config:
- kind: builtin # builtin | custom
- class_name: SimilarityEvaluator
- init: # constructor kwargs
- model_config:
- azure_endpoint: ${env:AZURE_OPENAI_ENDPOINT}
- azure_deployment: ${env:AZURE_OPENAI_DEPLOYMENT}
- input_mapping: # evaluator call kwargs
- query: $prompt
- response: $prediction
- ground_truth: $expected
- score_keys: # ordered keys to read numeric score
- - similarity
- - score
+version: 1
+agent: "https://support-dev.example.com/chat"
+dataset: .agentops/data/rag-smoke.jsonl
+response_source: agent
+
+protocol: http-json
+request_field: message
+response_fields:
+ response: answer
+ context: context
+
+thresholds:
+ groundedness: ">=3"
+ retrieval: ">=3"
+ coherence: ">=3"
```
-## 2) Global requirements by evaluator family
-
-- AI-assisted quality evaluators use a judge model (`model_config`) in Azure OpenAI/OpenAI schema.
-- Risk/safety evaluators and `GroundednessProEvaluator` use `azure_ai_project` instead of GPT deployment in `model_config`.
-- Agent evaluators require agent-style payloads (`query/response` as messages, and often tool metadata).
-- NLP evaluators (`F1`, `BLEU`, `GLEU`, `ROUGE`, `METEOR`) are non-LLM evaluators and usually need `response` + `ground_truth`.
-
-## 3) Built-in evaluators and required AgentOps parameters
-
-| Evaluator | Category | Typical required inputs | Backend init requirements | AgentOps `config` minimum |
-|---|---|---|---|---|
-| `CoherenceEvaluator` | General purpose | `query`, `response` | `model_config` (AI-assisted) | `kind: builtin`, `class_name`, `input_mapping(query,response)`, `score_keys` |
-| `FluencyEvaluator` | General purpose | `query`, `response` | `model_config` (AI-assisted) | same as above |
-| `SimilarityEvaluator` | Textual similarity | `query`, `response`, `ground_truth` | `model_config` (AI-assisted) | `input_mapping(query,response,ground_truth)` |
-| `F1ScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` |
-| `BleuScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` |
-| `GleuScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` |
-| `RougeScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | optional `rouge_type` in `init` | `input_mapping(response,ground_truth)` |
-| `MeteorScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | optional `alpha/beta/gamma/threshold` in `init` | `input_mapping(response,ground_truth)` |
-| `RetrievalEvaluator` | RAG | usually `query`, `response`, `context` | `model_config` (AI-assisted) | `input_mapping(query,response,context)` |
-| `DocumentRetrievalEvaluator` | RAG | retrieval outputs + `ground_truth` | check SDK class contract | explicit `input_mapping` recommended |
-| `GroundednessEvaluator` | RAG | `response`, `context` (optional `query`) | `model_config` (AI-assisted) | `input_mapping(response,context[,query])` |
-| `GroundednessProEvaluator` *(preview)* | RAG | `query/response/context` or conversation | `azure_ai_project` (not GPT model_config) | `init.azure_ai_project`, explicit `input_mapping` |
-| `RelevanceEvaluator` | RAG | `query`, `response` (optionally `context`) | `model_config` (AI-assisted) | `input_mapping(query,response[,context])` |
-| `ResponseCompletenessEvaluator` | RAG | `response`, `ground_truth` (often query optional) | AI-assisted quality path | explicit `input_mapping`, `score_keys` |
-| `HateUnfairnessEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` |
-| `SexualEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same |
-| `ViolenceEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same |
-| `SelfHarmEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same |
-| `ContentSafetyEvaluator` | Safety composite | query/response or conversation | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` |
-| `ProtectedMaterialEvaluator` | Safety | query/response or multimodal | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` |
-| `CodeVulnerabilityEvaluator` | Safety/risk | text/code response | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(response[,query])` |
-| `UngroundedAttributesEvaluator` | Safety/risk | text response | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(response[,query])` |
-| `IndirectAttackEvaluator` | Safety/risk | conversation-oriented input | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(conversation/query,response)` |
-| `IntentResolutionEvaluator` *(preview)* | Agent | `query`, `response` (string or message list) | agent evaluator path | `input_mapping(query,response[,tool_definitions])` |
-| `TaskAdherenceEvaluator` *(preview)* | Agent | `query`, `response` (string or message list) | agent evaluator path | `input_mapping(query,response[,tool_calls])` |
-| `ToolCallAccuracyEvaluator` *(preview)* | Agent | `query`; plus `response` or `tool_calls`; `tool_definitions` required | agent evaluator path | `input_mapping(query,response,tool_calls,tool_definitions)` |
-| `TaskCompletionEvaluator` *(preview)* | Agent | agent run/conversation payload | preview; use latest SDK docs | explicit `input_mapping`, explicit `score_keys` |
-| `TaskNavigationEfficiencyEvaluator` *(preview)* | Agent | tool/call sequence + expected path context | preview; evolving | explicit `input_mapping`, explicit `score_keys` |
-| `ToolSelectionEvaluator` *(preview)* | Agent | query/response + selected tools + tool defs | preview; evolving | explicit `input_mapping`, explicit `score_keys` |
-| `ToolInputAccuracyEvaluator` *(preview)* | Agent | tool args + tool defs + context | preview; evolving | explicit `input_mapping`, explicit `score_keys` |
-| `ToolOutputUtilizationEvaluator` *(preview)* | Agent | tool outputs + final response | preview; evolving | explicit `input_mapping`, explicit `score_keys` |
-| `ToolCallSuccessEvaluator` *(preview)* | Agent | tool execution results/status | preview; evolving | explicit `input_mapping`, explicit `score_keys` |
-| `QAEvaluator` | Composite quality | `query`, `response`, `ground_truth`, `context` | `model_config` (AI-assisted composite) | `input_mapping(query,response,ground_truth,context)` |
-| `AzureOpenAILabelGrader` | Azure OpenAI grader | template-driven (often conversation/query/response) | grader init requires template/model config | explicit `init` + explicit `input_mapping` |
-| `AzureOpenAIStringCheckGrader` | Azure OpenAI grader | template-driven text fields | grader init requires template | explicit `init` + explicit `input_mapping` |
-| `AzureOpenAITextSimilarityGrader` | Azure OpenAI grader | text + `ground_truth` equivalent | grader init requires template/model config | explicit `init` + explicit `input_mapping` |
-| `AzureOpenAIGrader` | Azure OpenAI grader | template-defined | grader init requires rubric/template | explicit `init` + explicit `input_mapping` |
-
-## 4) Practical rules for AgentOps bundles
-
-- Always set `source: foundry` for Foundry SDK evaluators.
-- For preview evaluators, always provide explicit:
- - `config.class_name`
- - `config.input_mapping`
- - `config.score_keys`
-- Prefer explicit `input_mapping` even when defaults might work.
-- Keep `thresholds[].evaluator` exactly equal to `evaluators[].name`.
-- For agent evaluators, use structured fields in dataset rows (messages, tool calls, tool definitions) and map with `$row.`.
-
-## 5) Examples by evaluator type
-
-The following examples show one practical bundle snippet for each evaluator family used in AgentOps:
-
-- `5.1` AI-assisted quality evaluators (`model_config`)
-- `5.2` Risk/safety evaluators (`azure_ai_project`)
-- `5.3` Agent evaluators (message/tool payloads)
-- `5.4` NLP evaluators (non-LLM)
-
-## 5.1) Example for AI-assisted quality evaluator (`model_config`)
+Use `evaluators:` only when you want to override the automatic choice:
```yaml
evaluators:
- - name: RelevanceEvaluator
- source: foundry
- enabled: true
- config:
- kind: builtin
- class_name: RelevanceEvaluator
- init:
- model_config:
- azure_endpoint: ${env:AZURE_OPENAI_ENDPOINT}
- azure_deployment: ${env:AZURE_OPENAI_DEPLOYMENT}
- input_mapping:
- query: $prompt
- response: $prediction
- score_keys:
- - relevance
- - score
+ - GroundednessEvaluator
+ - RetrievalEvaluator
+ - RelevanceEvaluator
+```
-thresholds:
- - evaluator: RelevanceEvaluator
- criteria: ">="
- value: 3
+## Requirements
+
+| Family | What it checks | Common inputs |
+|---|---|---|
+| Quality judges | The answer is coherent, fluent, similar, complete, or relevant. | prompt, response, expected answer |
+| RAG judges | The answer uses retrieved context and the retrieval is useful. | prompt, response, context |
+| Safety judges | The answer avoids harmful or protected content. | prompt, response |
+| Agent judges | Tool use and agent workflow behavior are correct. | prompt, response, tool calls, tool definitions |
+| Local metrics | Scores that do not need a judge model. | response, expected answer, latency |
+
+## Parameters
+
+AgentOps uses a small set of logical inputs. The same logical input can come from
+a static dataset, a live HTTP response, or imported telemetry.
+
+| Logical input | Meaning | Common source |
+|---|---|---|
+| `query` | The user prompt. | `row.input` |
+| `response` | The target's final answer. | extracted response text |
+| `ground_truth` | The expected answer or acceptance criteria. | `row.expected` |
+| `response field` | Any value extracted through `response_fields`. | `$response.` |
+| `context` | Retrieved chunks, citations, or grounding text. | `row.context`, `$response.context`, `$retrieved_context`, or `$retrieved_context_items` |
+| `tool_calls` | Tools called by the agent. | endpoint response or dataset row |
+| `tool_definitions` | Tool schemas available to the agent. | dataset row |
+| `trace_id` | Trace lineage for review and troubleshooting. | `$telemetry.trace_id` |
+
+## Rules
+
+The mapping rules are intentionally boring:
+
+1. `input` in the dataset becomes the evaluator `query`.
+2. The extracted target answer becomes `response`.
+3. `expected` in the dataset becomes `ground_truth`.
+4. `context` in the dataset becomes evaluator `context`.
+5. For grey-box HTTP, `response_fields.response` supplies the final answer.
+6. For grey-box HTTP, `response_fields.context` can supply `$response.context`.
+7. `$retrieved_context` and `$retrieved_context_items` expose retrieval context
+ in the evaluator placeholder format.
+8. `$telemetry.trace_id` exposes imported telemetry lineage when it exists.
+9. Tool fields are used only when the dataset or response includes tool data.
+
+For RAG, prefer a live context from the response when the endpoint can return it.
+That gives the judge the same evidence the agent used for the answer. Use static
+`row.context` when you want a fixed, hand-authored reference context.
+
+## Examples
+
+Static dataset row:
+
+```json
+{"input":"What is the refund window?","expected":"Customers can request a refund within 30 days.","context":"Refunds are available for 30 days after purchase."}
```
-## 5.2) Example for risk/safety evaluator (`azure_ai_project`)
+Static dataset config:
```yaml
-evaluators:
- - name: ViolenceEvaluator
- source: foundry
- enabled: true
- config:
- kind: builtin
- class_name: ViolenceEvaluator
- init:
- azure_ai_project: ${env:AZURE_AI_FOUNDRY_PROJECT_ENDPOINT}
- input_mapping:
- query: $prompt
- response: $prediction
- score_keys:
- - violence
- - score
-
-thresholds:
- - evaluator: ViolenceEvaluator
- criteria: "<="
- value: 2
+response_source: dataset
```
-`GroundednessProEvaluator` follows the same pattern (`azure_ai_project` in `init`).
+Use `response_source: dataset` when each row already has a `response`,
+`prediction`, `output`, or `answer` value and AgentOps should evaluate that value
+instead of calling the target.
-## 5.3) Example for agent evaluator (agent payload + tools)
+Grey-box HTTP config:
```yaml
-evaluators:
- - name: ToolCallAccuracyEvaluator
- source: foundry
- enabled: true
- config:
- kind: builtin
- class_name: ToolCallAccuracyEvaluator
- input_mapping:
- query: $row.query_messages
- response: $row.response_messages
- tool_calls: $row.tool_calls
- tool_definitions: $row.tool_definitions
- score_keys:
- - tool_call_accuracy
- - score
+protocol: http-json
+request_field: message
+response_fields:
+ response: output.answer
+ context: output.retrieval.chunks
+```
-thresholds:
- - evaluator: ToolCallAccuracyEvaluator
- criteria: ">="
- value: 3
+Telemetry import:
+
+```powershell
+agentops telemetry validate prod-rag
+agentops telemetry preview prod-rag --rows 10
+agentops telemetry import prod-rag --apply
```
-## 5.4) Example for NLP evaluator (non-LLM)
+When comparing this page with raw SDK examples, use these mappings:
-```yaml
-evaluators:
- - name: F1ScoreEvaluator
- source: foundry
- enabled: true
- config:
- kind: builtin
- class_name: F1ScoreEvaluator
- input_mapping:
- response: $prediction
- ground_truth: $expected
- score_keys:
- - f1_score
- - score
+- Quality evaluators often show `model_config`. In AgentOps, set the judge model
+ with `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME`.
+- Safety evaluators often show `azure_ai_project`. In AgentOps, set the Foundry
+ project with `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` or `project_endpoint:`.
+- Agent evaluators need the agent payload to include tool calls and tool
+ definitions when you want tools to be judged.
+- NLP metrics are non-LLM checks over values such as `response` and
+ `ground_truth`.
-thresholds:
- - evaluator: F1ScoreEvaluator
- criteria: ">="
- value: 0.7
-```
+## Quality
+
+| Evaluator | Typical inputs | Notes |
+|---|---|---|
+| `CoherenceEvaluator` | `query`, `response` | Checks whether the answer is logically consistent. |
+| `FluencyEvaluator` | `response` | Checks language quality. |
+| `SimilarityEvaluator` | `query`, `response`, `ground_truth` | Compares the answer with the expected answer. |
+| `ResponseCompletenessEvaluator` | `query`, `response`, `ground_truth` | Checks whether the answer covers what was expected. |
+| `RelevanceEvaluator` | `query`, `response`, optional `context` | Useful for both chat and RAG quality. |
-## 6) Cloud Evaluation defaults
+Quality judges need a judge model deployment. Set
+`AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` when local or
+cloud evaluation needs one.
-AgentOps provides sensible defaults so you don't need to configure extra environment variables:
+## Safety
-| Setting | Default | Override |
+| Evaluator | Typical inputs | Notes |
+|---|---|---|
+| `ViolenceEvaluator` | `query`, `response` | Scores violent content risk. |
+| `SexualEvaluator` | `query`, `response` | Scores sexual content risk. |
+| `SelfHarmEvaluator` | `query`, `response` | Scores self-harm content risk. |
+| `HateUnfairnessEvaluator` | `query`, `response` | Scores hate and unfairness risk. |
+| `ProtectedMaterialEvaluator` | `query`, `response` | Checks protected material risk when supported by the SDK. |
+| `ContentSafetyEvaluator` | `query`, `response` | Composite safety path when supported by the SDK. |
+
+Safety judges require a Foundry project connection. Use
+`AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` or `project_endpoint:` in `agentops.yaml`.
+
+## Agent
+
+| Evaluator | Typical inputs | Notes |
+|---|---|---|
+| `ToolCallAccuracyEvaluator` | `query`, `tool_calls`, `tool_definitions` | Checks whether the expected tools were called. |
+| `IntentResolutionEvaluator` | `query`, `response`, `tool_definitions` | Checks whether the agent resolved the user's intent. |
+| `TaskAdherenceEvaluator` | `query`, `response`, `tool_definitions` | Checks whether the agent stayed on task. |
+| `TaskCompletionEvaluator` | conversation payload | Preview in some SDK versions. |
+| `ToolSelectionEvaluator` | tool selection plus tool definitions | Preview in some SDK versions. |
+| `ToolInputAccuracyEvaluator` | tool arguments plus tool definitions | Preview in some SDK versions. |
+
+Agent judges work best when the target returns tool telemetry or the dataset row
+contains expected tool calls. If the endpoint cannot expose tool calls, start
+with answer quality and RAG judges instead.
+
+## NLP
+
+| Evaluator | Typical inputs | Notes |
|---|---|---|
-| Judge model (AI-assisted evaluators) | A deployment you configure in your project | `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` env var |
-| Authentication | `DefaultAzureCredential` (passwordless) | `az login` locally, Managed Identity in Azure |
+| `F1ScoreEvaluator` | `response`, `ground_truth` | Good for exact reference checks. |
+| `BleuScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. |
+| `GleuScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. |
+| `RougeScoreEvaluator` | `response`, `ground_truth` | Optional summary similarity metric. |
+| `MeteorScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. |
+| `avg_latency_seconds` | elapsed time | AgentOps computes this locally. |
-## 7) Known caveats
+Local metrics are useful when you want a cheap deterministic signal. They are not
+a replacement for human review or RAG-specific judges.
+
+## Cloud defaults
+
+AgentOps keeps cloud evaluation setup minimal:
+
+| Setting | Default | Override |
+|---|---|---|
+| Authentication | `DefaultAzureCredential` | `az login` locally, managed identity in Azure, or federated identity in CI. |
+| Foundry project | `project_endpoint` or `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` | Set either value before running. |
+| Judge model | Project deployment selected by environment | `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME`. |
+| Publishing | Implicit for `execution: cloud` | `publish: true` for local runs that should upload metrics. |
-- Some agent evaluators listed in the latest Foundry docs are preview and can change name/signature.
-- Not all preview evaluators have stable Python API docs with full constructor/call signatures at any given time.
-- When a signature changes, update the evaluator override list in `agentops.yaml` (no code change is needed in AgentOps core; the runtime is generic).
+## Caveats
-**Last updated:** 2026-03-02 (UTC)
+- Foundry Evaluation SDK preview evaluators can change names or call signatures.
+- If the SDK changes an evaluator, keep the docs, catalog, and tests in sync.
+- `response_fields.response` is the final answer path for HTTP JSON responses.
+- `response_fields.context` is the retrieved context path for RAG evaluation.
+- Production trace imports need review before they become blocking release gates.
-Because Foundry Evaluation SDK and evaluator signatures evolve (especially preview features), review official docs before production rollout.
+**Last updated:** 2026-06-26 (UTC)
diff --git a/plugins/agentops/package.json b/plugins/agentops/package.json
index 2709db0b..725073fd 100644
--- a/plugins/agentops/package.json
+++ b/plugins/agentops/package.json
@@ -2,7 +2,7 @@
"name": "agentops-accelerator",
"displayName": "AgentOps Accelerator — Skills for GitHub Copilot",
"description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.",
- "version": "0.4.1",
+ "version": "0.6.0",
"publisher": "AgentOpsAccelerator",
"icon": "icon.png",
"license": "MIT",
diff --git a/plugins/agentops/plugin.json b/plugins/agentops/plugin.json
index 83087442..a40491af 100644
--- a/plugins/agentops/plugin.json
+++ b/plugins/agentops/plugin.json
@@ -1,7 +1,7 @@
{
"name": "agentops-accelerator",
"description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.",
- "version": "0.4.1",
+ "version": "0.6.0",
"author": {
"name": "AgentOps Accelerator",
"url": "https://github.com/Azure/agentops"
diff --git a/plugins/agentops/skills/agentops-workflow/SKILL.md b/plugins/agentops/skills/agentops-workflow/SKILL.md
index 90cb635b..39cba197 100644
--- a/plugins/agentops/skills/agentops-workflow/SKILL.md
+++ b/plugins/agentops/skills/agentops-workflow/SKILL.md
@@ -62,11 +62,11 @@ by discovering the whole Azure subscription.
- `azd env get-values` when `azure.yaml` exists and azd is available.
- `.github/workflows/agentops-*.yml`.
2. Read the generated workflows to determine exactly which GitHub environments
- and variables are needed. For the prompt-agent tutorial, `pr` normally
- means only `environment: dev`.
-3. Treat `dev` here as a GitHub Actions environment for OIDC and variables. It
- normally points at the Foundry project already configured by `agentops init`;
- it does not require creating a new Foundry project.
+ and variables are needed. For prompt-agent PR gates, `pr` uses
+ `environment: sandbox`; deploy workflows use `dev`, `qa`, or `production`.
+3. Treat these as GitHub Actions environments for OIDC and variables. `sandbox`
+ points at the Foundry authoring project. `dev` points at the first shared
+ post-merge project.
4. Proceed only when these values are known or deliberately chosen:
- GitHub `owner/repo`.
- workflow environment names from `jobs.*.environment`.
@@ -271,7 +271,7 @@ The full scaffold writes:
| Kind | GitHub Actions path | Azure DevOps path | Trigger | Environment |
|---|---|---|---|---|
-| `pr` | `.github/workflows/agentops-pr.yml` | `.azuredevops/pipelines/agentops-pr.yml` | PRs to `develop`, `release/**`, `main` | `dev` |
+| `pr` | `.github/workflows/agentops-pr.yml` | `.azuredevops/pipelines/agentops-pr.yml` | PRs to `develop`, `release/**`, `main` | `sandbox` for prompt-agent PR candidates, `dev` for generic PR gates |
| `dev` | `.github/workflows/agentops-deploy-dev.yml` | `.azuredevops/pipelines/agentops-deploy-dev.yml` | push to `develop` | `dev` |
| `qa` | `.github/workflows/agentops-deploy-qa.yml` | `.azuredevops/pipelines/agentops-deploy-qa.yml` | push to `release/**` | `qa` |
| `prod` | `.github/workflows/agentops-deploy-prod.yml` | `.azuredevops/pipelines/agentops-deploy-prod.yml` | push to `main` | `production` |
@@ -303,9 +303,10 @@ Useful flags:
### GitHub Actions
Read the generated workflow files and create only the GitHub Environments used
-by `jobs.*.environment`. For `pr`, that is usually only **`dev`**. For the full
-scaffold, create **`dev`**, **`qa`**, and **`production`**.
+by `jobs.*.environment`. For prompt-agent PR gates, create **`sandbox`**. For the
+full scaffold, create **`sandbox`**, **`dev`**, **`qa`**, and **`production`**.
+- **`sandbox`** - no extra protection. Store the PR candidate endpoint and OIDC variables here when generated jobs use `environment: sandbox`.
- **`dev`** - no extra protection. Store the OIDC variables here when the
generated jobs use `environment: dev`.
- **`qa`** - usually no required reviewers, but isolated variables for QA.
diff --git a/pyproject.toml b/pyproject.toml
index a449cf86..ea1396cd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"typer>=0.12,<1.0",
+ "click>=8.1,<9",
"pydantic>=2,<3",
"ruamel.yaml>=0.18,<1.0",
"azure-ai-projects>=2.0.1,<3.0",
diff --git a/src/agentops/cli/app.py b/src/agentops/cli/app.py
index 699aaefc..ab3c912b 100644
--- a/src/agentops/cli/app.py
+++ b/src/agentops/cli/app.py
@@ -79,6 +79,9 @@
"for the manual."
)
)
+telemetry_app = typer.Typer(
+ help="Import Azure Monitor telemetry into AgentOps datasets."
+)
app.add_typer(eval_app, name="eval")
app.add_typer(report_app, name="report")
app.add_typer(workflow_app, name="workflow")
@@ -90,6 +93,7 @@
app.add_typer(init_app, name="init")
app.add_typer(assert_app, name="assert")
app.add_typer(redteam_app, name="redteam")
+app.add_typer(telemetry_app, name="telemetry")
log = get_logger(__name__)
DEFAULT_REPORT_INPUT = Path(".agentops/results/latest/results.json")
@@ -2348,6 +2352,109 @@ def cmd_eval_promote_traces(
)
+@telemetry_app.command("validate")
+def cmd_telemetry_validate(
+ name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")],
+ config: Annotated[
+ Optional[Path],
+ typer.Option("--config", "-c", help="Path to agentops.yaml."),
+ ] = None,
+) -> None:
+ """Validate a named telemetry import without querying Azure."""
+
+ from agentops.core.config_loader import load_agentops_config
+ from agentops.services.telemetry_import import (
+ TelemetryImportError,
+ find_telemetry_import,
+ validate_telemetry_import,
+ )
+
+ try:
+ cfg = load_agentops_config(_resolve_eval_config_path(config))
+ item = find_telemetry_import(cfg, name)
+ warnings = validate_telemetry_import(item)
+ except (TelemetryImportError, ValueError) as exc:
+ typer.echo(_cli_error(str(exc)), err=True)
+ raise typer.Exit(1) from exc
+ typer.echo(_cli_ok(f"telemetry import {name!r} is valid"))
+ for warning in warnings:
+ typer.echo(_cli_warn(f"warning: {warning}"))
+
+
+@telemetry_app.command("preview")
+def cmd_telemetry_preview(
+ name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")],
+ rows: Annotated[int, typer.Option("--rows", min=1, help="Maximum rows to preview.")] = 10,
+ config: Annotated[
+ Optional[Path],
+ typer.Option("--config", "-c", help="Path to agentops.yaml."),
+ ] = None,
+) -> None:
+ """Query Azure Monitor and print a small dataset preview."""
+
+ from agentops.core.config_loader import load_agentops_config
+ from agentops.services.telemetry_import import (
+ TelemetryImportError,
+ find_telemetry_import,
+ preview_telemetry_import,
+ render_telemetry_import_preview,
+ )
+
+ try:
+ cfg = load_agentops_config(_resolve_eval_config_path(config))
+ item = find_telemetry_import(cfg, name)
+ preview = preview_telemetry_import(item, rows=rows, apply=False)
+ except (TelemetryImportError, ValueError) as exc:
+ typer.echo(_cli_error(str(exc)), err=True)
+ raise typer.Exit(1) from exc
+ typer.echo(render_telemetry_import_preview(preview))
+
+
+@telemetry_app.command("import")
+def cmd_telemetry_import(
+ name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")],
+ apply: Annotated[
+ bool,
+ typer.Option("--apply", help="Write JSONL rows and manifest."),
+ ] = False,
+ rows: Annotated[
+ Optional[int],
+ typer.Option("--rows", min=1, help="Optional maximum rows to import."),
+ ] = None,
+ config: Annotated[
+ Optional[Path],
+ typer.Option("--config", "-c", help="Path to agentops.yaml."),
+ ] = None,
+) -> None:
+ """Import telemetry into the configured JSONL output path."""
+
+ from agentops.core.config_loader import load_agentops_config
+ from agentops.services.telemetry_import import (
+ TelemetryImportError,
+ find_telemetry_import,
+ preview_telemetry_import,
+ render_telemetry_import_preview,
+ )
+
+ if not apply:
+ typer.echo(
+ _cli_warn(
+ "Dry run only. Re-run with --apply to write the JSONL dataset and manifest."
+ )
+ )
+ try:
+ cfg = load_agentops_config(_resolve_eval_config_path(config))
+ item = find_telemetry_import(cfg, name)
+ preview = preview_telemetry_import(item, rows=rows, apply=apply)
+ except (TelemetryImportError, ValueError) as exc:
+ typer.echo(_cli_error(str(exc)), err=True)
+ raise typer.Exit(1) from exc
+ typer.echo(render_telemetry_import_preview(preview))
+ if apply:
+ typer.echo(_cli_updated(preview.output_path))
+ typer.echo(_cli_updated(preview.manifest_path))
+
+
def _resolve_eval_config_path(config: Path | None) -> Path:
if config is not None:
return config
diff --git a/src/agentops/core/agentops_config.py b/src/agentops/core/agentops_config.py
index dff21cc4..65acb1b9 100644
--- a/src/agentops/core/agentops_config.py
+++ b/src/agentops/core/agentops_config.py
@@ -78,6 +78,14 @@
#: Dataset shape used by the evaluator runtime or Foundry / azd recipes.
DatasetKind = Literal["auto", "single-turn", "multi-turn"]
+#: Where the local evaluator runtime gets the response text for each row.
+ResponseSource = Literal["agent", "dataset"]
+
+#: Production telemetry import providers and destinations.
+TelemetrySourceProvider = Literal["azure-monitor"]
+TelemetryTarget = Literal["application-insights", "log-analytics"]
+TelemetryLabelMode = Literal["self-similarity", "pending"]
+
#: Internal-only literal kept for the publisher dispatch table. Derived from
#: ``execution`` + ``publish`` via :meth:`AgentOpsConfig.publish_target`.
PublishTarget = Literal["foundry", "foundry_cloud"]
@@ -379,6 +387,116 @@ def _url_non_empty(cls, value: Optional[str]) -> Optional[str]:
return value
+# ---------------------------------------------------------------------------
+# Telemetry import configuration
+# ---------------------------------------------------------------------------
+
+
+class TelemetryTimeRangeConfig(BaseModel):
+ """Time window for a telemetry import query.
+
+ Users can either provide explicit ISO-ish ``from``/``to`` timestamps or a
+ relative ``lookback_days`` window. The service owns final KQL rendering so
+ users never pass arbitrary query text.
+ """
+
+ from_: Optional[str] = Field(None, alias="from")
+ to: Optional[str] = None
+ lookback_days: Optional[int] = Field(None, ge=1, le=90)
+
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+ @model_validator(mode="after")
+ def _validate_window(self) -> "TelemetryTimeRangeConfig":
+ explicit = self.from_ is not None or self.to is not None
+ if explicit and not (self.from_ and self.to):
+ raise ValueError("telemetry_imports.time_range requires both from and to")
+ if explicit and self.lookback_days is not None:
+ raise ValueError("telemetry_imports.time_range cannot mix from/to with lookback_days")
+ if not explicit and self.lookback_days is None:
+ self.lookback_days = 7
+ return self
+
+
+class TelemetryPrivacyConfig(BaseModel):
+ """Privacy controls applied before JSONL rows are written."""
+
+ redact_fields: List[str] = Field(
+ default_factory=lambda: ["authorization", "api_key", "token", "password", "secret"],
+ description="Case-insensitive field-name fragments to redact.",
+ )
+ max_field_length: int = Field(4000, ge=100, le=20000)
+ include_raw: bool = False
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class TelemetryOutputConfig(BaseModel):
+ """Output paths and labeling mode for generated dataset rows."""
+
+ path: Path = Field(Path(".agentops") / "data" / "telemetry-import.jsonl")
+ manifest_path: Optional[Path] = None
+ label_mode: TelemetryLabelMode = "self-similarity"
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class TelemetryImportConfig(BaseModel):
+ """Named telemetry import declaration.
+
+ The MVP intentionally keeps this declarative: users choose a supported
+ source/destination pair, field mappings, filters, privacy settings, and an
+ output file. The service generates the KQL.
+ """
+
+ name: str
+ source: TelemetrySourceProvider = "azure-monitor"
+ target: TelemetryTarget
+ resource_id: Optional[str] = None
+ workspace_id: Optional[str] = None
+ application_id: Optional[str] = None
+ connection_string: Optional[str] = None
+ time_range: TelemetryTimeRangeConfig = Field(default_factory=TelemetryTimeRangeConfig)
+ filters: Dict[str, str | List[str]] = Field(default_factory=dict)
+ fields: Dict[str, str] = Field(default_factory=dict)
+ privacy: TelemetryPrivacyConfig = Field(default_factory=TelemetryPrivacyConfig)
+ output: TelemetryOutputConfig = Field(default_factory=TelemetryOutputConfig)
+ max_rows: int = Field(100, ge=1, le=5000)
+
+ model_config = ConfigDict(extra="forbid")
+
+ @field_validator("name")
+ @classmethod
+ def _name_non_empty(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("telemetry_imports.name must be non-empty")
+ return value
+
+ @field_validator("resource_id", "workspace_id", "application_id", "connection_string")
+ @classmethod
+ def _optional_text_non_empty(cls, value: Optional[str]) -> Optional[str]:
+ if value is None:
+ return value
+ value = value.strip()
+ if not value:
+ raise ValueError("telemetry_imports resource identifiers must be non-empty")
+ return value
+
+ @model_validator(mode="after")
+ def _validate_target_ids(self) -> "TelemetryImportConfig":
+ if self.target == "log-analytics" and not self.workspace_id:
+ raise ValueError("telemetry_imports targeting log-analytics require workspace_id")
+ if self.target == "application-insights" and not (
+ self.resource_id or self.application_id or self.connection_string
+ ):
+ raise ValueError(
+ "telemetry_imports targeting application-insights require resource_id, "
+ "application_id, or connection_string"
+ )
+ return self
+
+
class PromptAgentBootstrap(BaseModel):
"""Bootstrap defaults for prompt-agent CI/CD when the target Foundry
project does not yet contain the seed agent referenced by ``agent``.
@@ -750,6 +868,13 @@ class AgentOpsConfig(BaseModel):
version: int = Field(..., description="Schema version. Must be 1.")
agent: str = Field(..., description="Target identifier (name:version, URL, or model:deployment)")
dataset: Path = Field(..., description="Path to a JSONL dataset file")
+ response_source: ResponseSource = Field(
+ "agent",
+ description=(
+ "Where local eval gets each response. 'agent' invokes the configured "
+ "target. 'dataset' uses each row's response or prediction value."
+ ),
+ )
dataset_kind: DatasetKind = Field(
"auto",
description=(
@@ -812,6 +937,7 @@ class AgentOpsConfig(BaseModel):
protocol: Optional[Protocol] = None
request_field: Optional[str] = None
response_field: Optional[str] = None
+ response_fields: Dict[str, str] = Field(default_factory=dict)
tool_calls_field: Optional[str] = None
response_fields: Dict[str, str] = Field(
default_factory=dict,
@@ -860,6 +986,10 @@ class AgentOpsConfig(BaseModel):
)
evaluators: Optional[List[EvaluatorOverride]] = None
+ telemetry_imports: List[TelemetryImportConfig] = Field(
+ default_factory=list,
+ description="Named Azure Monitor imports that generate AgentOps JSONL datasets.",
+ )
rubrics: List[RubricConfig] = Field(
default_factory=list,
description="Optional context-specific rubric evaluator definitions.",
@@ -1009,6 +1139,7 @@ def _validate_protocol_compat(self) -> "AgentOpsConfig":
if kind != "http_json" and (
self.request_field
or self.response_field
+ or self.response_fields
or self.tool_calls_field
or self.response_fields
or self.headers
diff --git a/src/agentops/pipeline/invocations.py b/src/agentops/pipeline/invocations.py
index f0a21866..3d6959c7 100644
--- a/src/agentops/pipeline/invocations.py
+++ b/src/agentops/pipeline/invocations.py
@@ -637,7 +637,7 @@ def _invoke_http_json(
) from exc
elapsed = time.perf_counter() - started
- response_path = config.response_field or "text"
+ response_path = config.response_field or config.response_fields.get("response") or "text"
response_text = _dot_path(payload, response_path)
if response_text is None:
for fallback in ("response", "output", "content", "message", "text"):
@@ -652,23 +652,23 @@ def _invoke_http_json(
if not isinstance(response_text, str):
response_text = json.dumps(response_text, ensure_ascii=False)
+ response_fields: Dict[str, Any] = {}
+ for name, path in config.response_fields.items():
+ value = _dot_path(payload, path)
+ if value is not None:
+ response_fields[name] = value
+
tool_calls: Optional[List[Any]] = None
if config.tool_calls_field:
extracted = _dot_path(payload, config.tool_calls_field)
if isinstance(extracted, list):
tool_calls = extracted
- captured: Dict[str, Any] = {}
- for name, path in (config.response_fields or {}).items():
- value = _dot_path(payload, path)
- if value is not None:
- captured[name] = value
-
return InvocationResult(
response=response_text.strip(),
latency_seconds=elapsed,
tool_calls=tool_calls,
- metadata={"response_fields": captured} if captured else {},
+ metadata={"response_fields": response_fields} if response_fields else {},
)
diff --git a/src/agentops/pipeline/orchestrator.py b/src/agentops/pipeline/orchestrator.py
index 72265fb3..f8bf8647 100644
--- a/src/agentops/pipeline/orchestrator.py
+++ b/src/agentops/pipeline/orchestrator.py
@@ -730,7 +730,8 @@ def _evaluate_row(
preview = str(row.get("input", "")).strip().replace("\n", " ")
if len(preview) > 80:
preview = preview[:77] + "..."
- progress(f"{label} invoking target: {preview!r}")
+ action = "using dataset response" if config.response_source == "dataset" else "invoking target"
+ progress(f"{label} {action}: {preview!r}")
expected = row.get("expected")
expected_text = str(expected) if expected is not None else None
@@ -740,18 +741,21 @@ def _evaluate_row(
expected_text=expected_text,
) as item_span:
try:
- with telemetry.agent_invoke_span(
- target="agent" if target.kind.startswith("foundry") else "model",
- model=target.deployment,
- agent_id=target.raw if target.kind.startswith("foundry") else None,
- agent_name=target.name,
- agent_version=target.version,
- ) as invoke_span:
- invocation = invocations.invoke(target, config, row, timeout=timeout)
- telemetry.set_agent_invoke_result(
- invoke_span,
- response_model=target.deployment,
- )
+ if config.response_source == "dataset":
+ invocation = _dataset_invocation(row)
+ else:
+ with telemetry.agent_invoke_span(
+ target="agent" if target.kind.startswith("foundry") else "model",
+ model=target.deployment,
+ agent_id=target.raw if target.kind.startswith("foundry") else None,
+ agent_name=target.name,
+ agent_version=target.version,
+ ) as invoke_span:
+ invocation = invocations.invoke(target, config, row, timeout=timeout)
+ telemetry.set_agent_invoke_result(
+ invoke_span,
+ response_model=target.deployment,
+ )
except Exception as exc: # noqa: BLE001
telemetry.set_eval_item_result(item_span, passed=False)
logger.debug("row %d invocation failed: %s", index, exc)
@@ -771,12 +775,17 @@ def _evaluate_row(
f"({tool_count} tool call(s)); scoring..."
)
+ response_fields = invocation.metadata.get("response_fields")
+ evaluator_row = row
+ if isinstance(response_fields, dict) and response_fields:
+ evaluator_row = {**row, "response": response_fields}
+
metrics: List[RowMetric] = []
captured_fields = invocation.metadata.get("response_fields") or {}
for evaluator in evaluators:
metric = runtime.run_evaluator(
evaluator,
- row=row,
+ row=evaluator_row,
response=invocation.response,
latency_seconds=invocation.latency_seconds,
actual_tool_calls=invocation.tool_calls,
@@ -828,18 +837,57 @@ def _format_metric(m: RowMetric) -> str:
scored = ", ".join(_format_metric(m) for m in metrics)
progress(f"{label} scored: {scored}")
+ result_context = (
+ response_fields.get("context")
+ if isinstance(response_fields, dict) and response_fields.get("context") is not None
+ else row.get("context")
+ )
+
return RowResult(
row_index=index,
input=str(row.get("input", "")),
expected=row.get("expected"),
response=invocation.response,
- context=captured_fields.get("context", row.get("context")),
+ context=_context_as_text(result_context),
latency_seconds=invocation.latency_seconds,
tool_calls=invocation.tool_calls,
metrics=metrics,
)
+def _dataset_invocation(row: Dict[str, Any]) -> invocations.InvocationResult:
+ """Build an invocation result from dataset columns without calling a target."""
+
+ response = row.get("response")
+ if response is None:
+ response = row.get("prediction")
+ if response is None:
+ raise ValueError(
+ "response_source: dataset requires each dataset row to contain "
+ "a response or prediction field"
+ )
+
+ tool_calls = row.get("actual_tool_calls")
+ if tool_calls is None:
+ tool_calls = row.get("tool_calls")
+ if tool_calls is not None and not isinstance(tool_calls, list):
+ tool_calls = None
+ return invocations.InvocationResult(
+ response=str(response),
+ latency_seconds=0.0,
+ tool_calls=tool_calls,
+ metadata={"response_source": "dataset"},
+ )
+
+
+def _context_as_text(value: Any) -> Optional[str]:
+ if value is None:
+ return None
+ if isinstance(value, str):
+ return value
+ return json.dumps(value, ensure_ascii=False)
+
+
# ---------------------------------------------------------------------------
# Aggregation
# ---------------------------------------------------------------------------
diff --git a/src/agentops/pipeline/runtime.py b/src/agentops/pipeline/runtime.py
index 64836b99..1d2fe15a 100644
--- a/src/agentops/pipeline/runtime.py
+++ b/src/agentops/pipeline/runtime.py
@@ -201,8 +201,11 @@ def load_evaluators(presets: List[EvaluatorPreset]) -> List[EvaluatorRuntime]:
"$prediction": "response",
"$expected": "expected",
"$context": "context",
+ "$retrieved_context": "retrieved_context",
+ "$retrieved_context_items": "retrieved_context_items",
"$tool_calls": "tool_calls",
"$tool_definitions": "tool_definitions",
+ "$telemetry.trace_id": "telemetry.trace_id",
}
@@ -301,6 +304,7 @@ def _resolve_kwargs(
response_fields: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
resolved: Dict[str, Any] = {}
+ row_response = row.get("response")
merged = {**row, "response": response, "input": row.get("input")}
captured = response_fields or {}
for kwarg, placeholder in mapping.items():
@@ -313,6 +317,8 @@ def _resolve_kwargs(
# returned alongside the answer on the same call.
name = placeholder[len("$response."):]
value = captured.get(name)
+ if value is None and isinstance(row_response, dict):
+ value = _lookup_placeholder(row_response, name)
if value is not None:
resolved[kwarg] = value
continue
@@ -325,16 +331,27 @@ def _resolve_kwargs(
if value is not None:
resolved[kwarg] = value
continue
- source_key = _PLACEHOLDERS.get(placeholder)
- if source_key is None:
+ source_path = _PLACEHOLDERS.get(placeholder)
+ if source_path is None and placeholder.startswith("$telemetry."):
+ source_path = placeholder[1:]
+ if source_path is None:
raise ValueError(f"unknown evaluator placeholder {placeholder!r}")
- value = merged.get(source_key)
+ value = _lookup_placeholder(merged, source_path)
if value is None:
continue
resolved[kwarg] = value
return resolved
+def _lookup_placeholder(data: Dict[str, Any], path: str) -> Any:
+ current: Any = data
+ for part in path.split("."):
+ if not isinstance(current, dict):
+ return None
+ current = current.get(part)
+ return current
+
+
def _extract_score(payload: Any, score_key: str) -> Optional[float]:
if payload is None:
return None
diff --git a/src/agentops/services/telemetry_import.py b/src/agentops/services/telemetry_import.py
new file mode 100644
index 00000000..286778c5
--- /dev/null
+++ b/src/agentops/services/telemetry_import.py
@@ -0,0 +1,550 @@
+"""Import Azure Monitor telemetry into AgentOps JSONL datasets.
+
+The module has two halves:
+
+* a pure transformer that maps telemetry rows into AgentOps dataset rows
+* a thin Azure Monitor query wrapper with lazy SDK imports
+
+Users never provide raw KQL. The query builder only accepts structured time
+ranges, field mappings, filters, and row limits from ``agentops.yaml``.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import re
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Iterable, Optional
+
+from agentops.core.agentops_config import AgentOpsConfig, TelemetryImportConfig
+
+DEFAULT_MAX_ROWS = 100
+MAX_ROWS_CAP = 5000
+
+_DEFAULT_FIELD_CANDIDATES: dict[str, tuple[str, ...]] = {
+ "input": (
+ "input",
+ "query",
+ "prompt",
+ "message",
+ "user_message",
+ "customDimensions.input",
+ "customDimensions.query",
+ "customDimensions.prompt",
+ "customDimensions.gen_ai.prompt",
+ ),
+ "response": (
+ "response",
+ "prediction",
+ "output",
+ "answer",
+ "completion",
+ "assistant_message",
+ "customDimensions.response",
+ "customDimensions.prediction",
+ "customDimensions.output",
+ "customDimensions.gen_ai.completion",
+ ),
+ "context": (
+ "context",
+ "retrieved_context",
+ "grounding",
+ "customDimensions.context",
+ "customDimensions.retrieved_context",
+ "customDimensions.grounding",
+ ),
+ "retrieved_context_items": (
+ "retrieved_context_items",
+ "context_items",
+ "customDimensions.retrieved_context_items",
+ "customDimensions.context_items",
+ ),
+ "tool_calls": ("tool_calls", "customDimensions.tool_calls"),
+ "trace_id": ("trace_id", "operation_Id", "operationId"),
+ "turn_id": ("turn_id", "span_id", "id", "customDimensions.turn_id"),
+ "timestamp": ("timestamp", "TimeGenerated", "time"),
+}
+
+_QUERY_COLUMNS = (
+ "timestamp",
+ "operation_Id = column_ifexists('operation_Id', '')",
+ "operationId = column_ifexists('operationId', '')",
+ "id = column_ifexists('id', '')",
+ "name = column_ifexists('name', '')",
+ "message = column_ifexists('message', '')",
+ "duration = column_ifexists('duration', '')",
+ "success = column_ifexists('success', '')",
+ "customDimensions = column_ifexists('customDimensions', dynamic({}))",
+)
+
+
+class TelemetryImportError(RuntimeError):
+ """Raised when a telemetry import cannot be validated, queried, or written."""
+
+
+@dataclass(frozen=True)
+class TelemetryImportPreview:
+ """Result of validating/querying/transforming one telemetry import."""
+
+ config: TelemetryImportConfig
+ output_path: Path
+ manifest_path: Path
+ rows: list[dict[str, Any]]
+ skipped: int = 0
+ deduped: int = 0
+ truncated: bool = False
+ warnings: list[str] = field(default_factory=list)
+
+
+def find_telemetry_import(
+ config: AgentOpsConfig,
+ name: str,
+) -> TelemetryImportConfig:
+ """Return a named telemetry import or raise a friendly error."""
+
+ for item in config.telemetry_imports:
+ if item.name == name:
+ return item
+ available = ", ".join(item.name for item in config.telemetry_imports) or "none"
+ raise TelemetryImportError(
+ f"telemetry import {name!r} was not found in agentops.yaml. "
+ f"Available imports: {available}."
+ )
+
+
+def validate_telemetry_import(config: TelemetryImportConfig) -> list[str]:
+ """Validate service-level constraints and return non-fatal warnings."""
+
+ warnings: list[str] = []
+ if config.output.label_mode == "self-similarity":
+ warnings.append(
+ "Generated rows use production responses as expected values for drift "
+ "detection, not human-verified ground truth."
+ )
+ return warnings
+
+
+def preview_telemetry_import(
+ config: TelemetryImportConfig,
+ *,
+ rows: Optional[int] = None,
+ apply: bool = False,
+) -> TelemetryImportPreview:
+ """Query Azure Monitor, transform rows, and optionally write JSONL output."""
+
+ validate_telemetry_import(config)
+ raw_rows = query_azure_monitor(config, rows=rows)
+ preview = transform_telemetry_rows(config, raw_rows, rows=rows)
+ if apply:
+ write_telemetry_import(preview)
+ return preview
+
+
+def transform_telemetry_rows(
+ config: TelemetryImportConfig,
+ telemetry_rows: Iterable[dict[str, Any]],
+ *,
+ rows: Optional[int] = None,
+) -> TelemetryImportPreview:
+ """Pure transformation from telemetry records to AgentOps dataset rows."""
+
+ limit = _bounded_rows(rows if rows is not None else config.max_rows)
+ output_path = config.output.path
+ manifest_path = config.output.manifest_path or output_path.with_name(
+ f"{output_path.stem}-manifest.json"
+ )
+ warnings = validate_telemetry_import(config)
+ converted: list[dict[str, Any]] = []
+ skipped = 0
+ deduped = 0
+ seen: set[tuple[str, str]] = set()
+
+ for raw in telemetry_rows:
+ if len(converted) >= limit:
+ break
+ row = _telemetry_row_to_agentops_row(config, raw)
+ if row is None:
+ skipped += 1
+ continue
+ telemetry = row.get("telemetry")
+ trace_id = ""
+ turn_id = ""
+ if isinstance(telemetry, dict):
+ trace_id = str(telemetry.get("trace_id") or "")
+ turn_id = str(telemetry.get("turn_id") or "")
+ key = (trace_id or row["input"], turn_id or row.get("response", ""))
+ if key in seen:
+ deduped += 1
+ continue
+ seen.add(key)
+ converted.append(row)
+
+ truncated = len(converted) >= limit
+ if not converted:
+ warnings.append("No telemetry rows contained both input and response text.")
+ return TelemetryImportPreview(
+ config=config,
+ output_path=output_path,
+ manifest_path=manifest_path,
+ rows=converted,
+ skipped=skipped,
+ deduped=deduped,
+ truncated=truncated,
+ warnings=warnings,
+ )
+
+
+def write_telemetry_import(preview: TelemetryImportPreview) -> None:
+ """Write JSONL rows and a small manifest next to the output."""
+
+ preview.output_path.parent.mkdir(parents=True, exist_ok=True)
+ with preview.output_path.open("w", encoding="utf-8") as handle:
+ for row in preview.rows:
+ handle.write(json.dumps(row, ensure_ascii=False) + "\n")
+
+ trace_ids = [
+ str(row.get("telemetry", {}).get("trace_id"))
+ for row in preview.rows
+ if isinstance(row.get("telemetry"), dict) and row["telemetry"].get("trace_id")
+ ]
+ manifest = {
+ "version": 1,
+ "generated_at": datetime.now(timezone.utc).isoformat(),
+ "import": preview.config.name,
+ "source": preview.config.source,
+ "target": preview.config.target,
+ "output_path": str(preview.output_path),
+ "rows": len(preview.rows),
+ "skipped": preview.skipped,
+ "deduped": preview.deduped,
+ "truncated": preview.truncated,
+ "trace_ids": trace_ids,
+ "warnings": preview.warnings,
+ }
+ preview.manifest_path.write_text(
+ json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
+ encoding="utf-8",
+ )
+
+
+def render_telemetry_import_preview(preview: TelemetryImportPreview) -> str:
+ """Render concise CLI output."""
+
+ lines = [
+ "AgentOps telemetry import",
+ f"Import: {preview.config.name}",
+ f"Target: {preview.config.target}",
+ f"Output: {preview.output_path}",
+ "",
+ "Summary",
+ f" rows {len(preview.rows)}",
+ f" skipped {preview.skipped}",
+ f" deduped {preview.deduped}",
+ f" truncated {str(preview.truncated).lower()}",
+ ]
+ if preview.warnings:
+ lines.append("")
+ lines.append("Warnings")
+ lines.extend(f" - {warning}" for warning in preview.warnings)
+ if preview.rows:
+ lines.append("")
+ lines.append("Sample rows")
+ for index, row in enumerate(preview.rows[:3], start=1):
+ lines.append(f" {index}. {str(row.get('input', ''))[:120]}")
+ return "\n".join(lines) + "\n"
+
+
+def query_azure_monitor(
+ config: TelemetryImportConfig,
+ *,
+ rows: Optional[int] = None,
+) -> list[dict[str, Any]]:
+ """Run the generated KQL against Azure Monitor with lazy SDK imports."""
+
+ try:
+ from azure.identity import DefaultAzureCredential # noqa: WPS433
+ except ImportError as exc:
+ raise TelemetryImportError(
+ "Telemetry import requires Azure authentication packages. Install "
+ "them with: python -m pip install azure-identity azure-monitor-query"
+ ) from exc
+
+ kql = build_telemetry_kql(config, rows=rows)
+ credential = DefaultAzureCredential(
+ exclude_developer_cli_credential=True,
+ process_timeout=30,
+ )
+ try:
+ if config.target == "log-analytics":
+ from azure.monitor.query import LogsQueryClient # noqa: WPS433
+
+ client = LogsQueryClient(credential)
+ workspace_id = _resolve_value(config.workspace_id, "workspace_id")
+ response = client.query_workspace(workspace_id, kql, timespan=None)
+ return _flatten_logs_response(response)
+ if config.resource_id:
+ from azure.monitor.query import LogsQueryClient # noqa: WPS433
+
+ client = LogsQueryClient(credential)
+ resource_id = _resolve_value(config.resource_id, "resource_id")
+ response = client.query_resource(resource_id, kql, timespan=None)
+ return _flatten_logs_response(response)
+ app_id = _application_id(config)
+ token = credential.get_token("https://api.applicationinsights.io/.default").token
+ return _query_application_insights(app_id, token, kql)
+ except ImportError as exc:
+ raise TelemetryImportError(
+ "Telemetry import with resource_id/workspace_id requires the Azure "
+ "Monitor Query SDK. Install it with: python -m pip install "
+ "azure-monitor-query"
+ ) from exc
+ except TelemetryImportError:
+ raise
+ except Exception as exc: # noqa: BLE001
+ raise TelemetryImportError(f"Azure Monitor query failed: {exc}") from exc
+
+
+def build_telemetry_kql(
+ config: TelemetryImportConfig,
+ *,
+ rows: Optional[int] = None,
+) -> str:
+ """Build safe KQL from structured config only."""
+
+ limit = _bounded_rows(rows if rows is not None else config.max_rows)
+ clauses = ["union isfuzzy=true requests, dependencies, traces"]
+ clauses.append(f"| extend timestamp = {_timestamp_expr()}")
+ clauses.append(_time_clause(config))
+ for key, value in sorted(config.filters.items()):
+ clauses.append(_filter_clause(key, value))
+ columns = ", ".join(_QUERY_COLUMNS)
+ clauses.append(f"| project {columns}")
+ clauses.append("| order by timestamp desc")
+ clauses.append(f"| take {limit}")
+ return "\n".join(clauses)
+
+
+def _telemetry_row_to_agentops_row(
+ config: TelemetryImportConfig,
+ raw: dict[str, Any],
+) -> Optional[dict[str, Any]]:
+ input_text = _mapped_text(config, raw, "input")
+ response_text = _mapped_text(config, raw, "response")
+ if not input_text or not response_text:
+ return None
+
+ label_mode = config.output.label_mode
+ telemetry = {
+ "trace_id": _mapped_text(config, raw, "trace_id"),
+ "turn_id": _mapped_text(config, raw, "turn_id"),
+ "timestamp": _mapped_text(config, raw, "timestamp"),
+ "source": config.source,
+ "target": config.target,
+ "import": config.name,
+ }
+ row: dict[str, Any] = {
+ "input": _clean_value(input_text, config),
+ "response": _clean_value(response_text, config),
+ "prediction": _clean_value(response_text, config),
+ "expected": _clean_value(response_text, config) if label_mode == "self-similarity" else "",
+ "telemetry": {k: v for k, v in telemetry.items() if v not in (None, "")},
+ "metadata": {
+ "source": "azure_monitor_telemetry",
+ "label_mode": label_mode,
+ "needs_review": True,
+ },
+ }
+ context = _mapped_value(config, raw, "context")
+ if context not in (None, "", [], {}):
+ row["context"] = _clean_value(context, config)
+ row["retrieved_context"] = row["context"]
+ context_items = _mapped_value(config, raw, "retrieved_context_items")
+ if context_items not in (None, "", [], {}):
+ row["retrieved_context_items"] = _clean_value(context_items, config)
+ tool_calls = _mapped_value(config, raw, "tool_calls")
+ if tool_calls not in (None, "", [], {}):
+ row["tool_calls"] = _clean_value(tool_calls, config)
+ if config.privacy.include_raw:
+ row["raw"] = _clean_value(raw, config)
+ return row
+
+
+def _mapped_text(config: TelemetryImportConfig, raw: dict[str, Any], name: str) -> Optional[str]:
+ value = _mapped_value(config, raw, name)
+ if value is None:
+ return None
+ if isinstance(value, str):
+ value = value.strip()
+ return value or None
+ if isinstance(value, (dict, list)):
+ text = json.dumps(value, ensure_ascii=False)
+ return text if text not in ("{}", "[]") else None
+ text = str(value).strip()
+ return text or None
+
+
+def _mapped_value(config: TelemetryImportConfig, raw: dict[str, Any], name: str) -> Any:
+ mapping = config.fields.get(name)
+ if mapping:
+ return _lookup(raw, mapping)
+ for candidate in _DEFAULT_FIELD_CANDIDATES.get(name, ()):
+ value = _lookup(raw, candidate)
+ if value not in (None, "", [], {}):
+ return value
+ return None
+
+
+def _lookup(data: dict[str, Any], path: str) -> Any:
+ current: Any = data
+ for part in path.split("."):
+ if not isinstance(current, dict):
+ return None
+ current = current.get(part)
+ return current
+
+
+def _clean_value(value: Any, config: TelemetryImportConfig, key: str = "") -> Any:
+ lowered = key.lower()
+ if any(fragment.lower() in lowered for fragment in config.privacy.redact_fields):
+ return "[redacted]"
+ if isinstance(value, dict):
+ return {k: _clean_value(v, config, str(k)) for k, v in value.items()}
+ if isinstance(value, list):
+ return [_clean_value(item, config, key) for item in value]
+ if isinstance(value, str) and len(value) > config.privacy.max_field_length:
+ return value[: config.privacy.max_field_length] + "...[truncated]"
+ return value
+
+
+def _flatten_logs_response(response: Any) -> list[dict[str, Any]]:
+ tables = getattr(response, "tables", None) or []
+ if not tables:
+ return []
+ table = tables[0]
+ columns: list[str] = []
+ for column in getattr(table, "columns", None) or []:
+ name = getattr(column, "name", None) if not isinstance(column, dict) else column.get("name")
+ if isinstance(name, str):
+ columns.append(name)
+ rows: list[dict[str, Any]] = []
+ for raw in getattr(table, "rows", None) or []:
+ rows.append(dict(zip(columns, raw)))
+ return rows
+
+
+def _application_id(config: TelemetryImportConfig) -> str:
+ if config.application_id:
+ return _resolve_value(config.application_id, "application_id")
+ if config.connection_string:
+ connection_string = _resolve_value(config.connection_string, "connection_string")
+ match = re.search(r"ApplicationId=([0-9a-fA-F-]+)", connection_string)
+ if match:
+ return match.group(1)
+ raise TelemetryImportError(
+ "application-insights imports require resource_id, application_id, or "
+ "a connection_string containing ApplicationId"
+ )
+
+
+def _query_application_insights(app_id: str, bearer: str, kql: str) -> list[dict[str, Any]]:
+ import json as _json
+ from urllib import request
+
+ body = _json.dumps({"query": kql}).encode("utf-8")
+ req = request.Request(
+ url=f"https://api.applicationinsights.io/v1/apps/{app_id}/query",
+ data=body,
+ headers={
+ "Authorization": f"Bearer {bearer}",
+ "Content-Type": "application/json",
+ },
+ method="POST",
+ )
+ with request.urlopen(req, timeout=30) as response: # noqa: S310
+ parsed = _json.loads(response.read())
+ if isinstance(parsed, dict) and parsed.get("error"):
+ err = parsed["error"]
+ message = err.get("message") if isinstance(err, dict) else str(err)
+ raise TelemetryImportError(f"Application Insights query failed: {message}")
+ tables = parsed.get("tables") if isinstance(parsed, dict) else None
+ if not tables:
+ return []
+ table = tables[0]
+ columns = [column.get("name") for column in table.get("columns", [])]
+ return [dict(zip(columns, row)) for row in table.get("rows", [])]
+
+
+def _time_clause(config: TelemetryImportConfig) -> str:
+ tr = config.time_range
+ if tr.from_ and tr.to:
+ return (
+ f"| where timestamp between (datetime({_kql_string(tr.from_)}) .. "
+ f"datetime({_kql_string(tr.to)}))"
+ )
+ days = tr.lookback_days or 7
+ return f"| where timestamp >= ago({days}d)"
+
+
+def _filter_clause(key: str, value: str | list[str]) -> str:
+ expr = _safe_column_expr(key)
+ values = value if isinstance(value, list) else [value]
+ escaped = ", ".join(_kql_string(str(item)) for item in values)
+ if len(values) == 1:
+ return f"| where {expr} == {escaped}"
+ return f"| where {expr} in ({escaped})"
+
+
+def _safe_column_expr(key: str) -> str:
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?", key):
+ raise TelemetryImportError(
+ f"unsafe telemetry filter field {key!r}; use a column name or customDimensions.name"
+ )
+ if key.startswith("customDimensions."):
+ subkey = key.split(".", 1)[1]
+ return (
+ "tostring(column_ifexists('customDimensions', dynamic({}))"
+ f"[{_kql_string(subkey)}])"
+ )
+ return f"tostring(column_ifexists({_kql_string(key)}, ''))"
+
+
+def _timestamp_expr() -> str:
+ return (
+ "coalesce("
+ "column_ifexists('timestamp', datetime(null)), "
+ "column_ifexists('TimeGenerated', datetime(null)), "
+ "column_ifexists('time', datetime(null))"
+ ")"
+ )
+
+
+def _kql_string(value: str) -> str:
+ return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
+
+
+def _resolve_value(value: Optional[str], label: str) -> str:
+ if not value:
+ raise TelemetryImportError(f"telemetry import is missing {label}")
+ value = value.strip()
+ env_name: Optional[str] = None
+ if value.startswith("env:"):
+ env_name = value[4:]
+ elif value.startswith("$") and len(value) > 1:
+ env_name = value[1:].strip("{}")
+ if env_name:
+ resolved = os.getenv(env_name)
+ if not resolved:
+ raise TelemetryImportError(
+ f"environment variable {env_name} referenced by {label} is not set"
+ )
+ return resolved
+ return value
+
+
+def _bounded_rows(rows: int) -> int:
+ if rows <= 0:
+ raise TelemetryImportError("rows must be greater than zero")
+ return min(rows, MAX_ROWS_CAP)
diff --git a/tests/unit/test_agentops_config.py b/tests/unit/test_agentops_config.py
index 3b3bacad..2a4676d2 100644
--- a/tests/unit/test_agentops_config.py
+++ b/tests/unit/test_agentops_config.py
@@ -141,6 +141,111 @@ def test_minimal_config(self, tmp_path) -> None:
assert cfg.version == 1
assert cfg.agent == "my-rag:3"
assert cfg.thresholds == {}
+ assert cfg.response_source == "agent"
+ assert cfg.telemetry_imports == []
+
+ def test_accepts_telemetry_import_config(self) -> None:
+ cfg = AgentOpsConfig.model_validate(
+ {
+ "version": 1,
+ "agent": "my-rag:3",
+ "dataset": "./qa.jsonl",
+ "response_source": "dataset",
+ "telemetry_imports": [
+ {
+ "name": "prod",
+ "source": "azure-monitor",
+ "target": "application-insights",
+ "resource_id": "$APPINSIGHTS_RESOURCE_ID",
+ "time_range": {"lookback_days": 14},
+ "filters": {"customDimensions.agent": "support"},
+ "fields": {
+ "input": "customDimensions.question",
+ "response": "customDimensions.answer",
+ },
+ "privacy": {"redact_fields": ["token"], "max_field_length": 500},
+ "output": {
+ "path": ".agentops/data/prod.jsonl",
+ "label_mode": "pending",
+ },
+ }
+ ],
+ }
+ )
+
+ item = cfg.telemetry_imports[0]
+ assert cfg.response_source == "dataset"
+ assert item.name == "prod"
+ assert item.source == "azure-monitor"
+ assert item.target == "application-insights"
+ assert item.resource_id == "$APPINSIGHTS_RESOURCE_ID"
+ assert item.time_range.lookback_days == 14
+ assert item.output.label_mode == "pending"
+
+ def test_telemetry_import_rejects_unknown_fields(self) -> None:
+ with pytest.raises(ValidationError):
+ AgentOpsConfig.model_validate(
+ {
+ "version": 1,
+ "agent": "my-rag:3",
+ "dataset": "./qa.jsonl",
+ "telemetry_imports": [
+ {
+ "name": "prod",
+ "target": "log-analytics",
+ "workspace_id": "workspace",
+ "surprise": True,
+ }
+ ],
+ }
+ )
+
+ def test_telemetry_import_time_range_requires_one_mode(self) -> None:
+ with pytest.raises(ValidationError, match="cannot mix"):
+ AgentOpsConfig.model_validate(
+ {
+ "version": 1,
+ "agent": "my-rag:3",
+ "dataset": "./qa.jsonl",
+ "telemetry_imports": [
+ {
+ "name": "prod",
+ "target": "log-analytics",
+ "workspace_id": "workspace",
+ "time_range": {
+ "from": "2026-06-01T00:00:00Z",
+ "to": "2026-06-02T00:00:00Z",
+ "lookback_days": 7,
+ },
+ }
+ ],
+ }
+ )
+
+ def test_telemetry_import_accepts_explicit_time_range(self) -> None:
+ cfg = AgentOpsConfig.model_validate(
+ {
+ "version": 1,
+ "agent": "my-rag:3",
+ "dataset": "./qa.jsonl",
+ "telemetry_imports": [
+ {
+ "name": "prod",
+ "target": "log-analytics",
+ "workspace_id": "workspace",
+ "time_range": {
+ "from": "2026-06-01T00:00:00Z",
+ "to": "2026-06-02T00:00:00Z",
+ },
+ }
+ ],
+ }
+ )
+
+ time_range = cfg.telemetry_imports[0].time_range
+ assert time_range.from_ == "2026-06-01T00:00:00Z"
+ assert time_range.to == "2026-06-02T00:00:00Z"
+ assert time_range.lookback_days is None
def test_resolved_target(self) -> None:
cfg = AgentOpsConfig(version=1, agent="my-rag:3", dataset="./qa.jsonl")
@@ -472,8 +577,10 @@ def test_http_fields_allowed_for_http_target(self) -> None:
dataset="./qa.jsonl",
request_field="message",
response_field="text",
+ response_fields={"context": "retrieval.context"},
)
assert cfg.request_field == "message"
+ assert cfg.response_fields == {"context": "retrieval.context"}
def test_http_fields_rejected_for_prompt_agent(self) -> None:
with pytest.raises(ValidationError, match="HTTP/JSON"):
@@ -481,7 +588,7 @@ def test_http_fields_rejected_for_prompt_agent(self) -> None:
version=1,
agent="my-rag:3",
dataset="./qa.jsonl",
- request_field="message",
+ response_fields={"context": "context"},
)
def test_streaming_fields_allowed_for_http_target(self) -> None:
diff --git a/tests/unit/test_cli_commands.py b/tests/unit/test_cli_commands.py
index 0c6b2a68..3bb6feb7 100644
--- a/tests/unit/test_cli_commands.py
+++ b/tests/unit/test_cli_commands.py
@@ -1,6 +1,7 @@
from typer.testing import CliRunner
from agentops.cli.app import app
+from agentops.services.telemetry_import import TelemetryImportPreview
runner = CliRunner()
@@ -83,3 +84,102 @@ def test_agent_command_group_wired() -> None:
stripped = _strip_ansi(result.stdout)
assert "analyze" in stripped
assert "serve" in stripped
+
+
+def test_telemetry_validate_uses_named_import(tmp_path, monkeypatch) -> None:
+ config = tmp_path / "agentops.yaml"
+ config.write_text(
+ "\n".join(
+ [
+ "version: 1",
+ "agent: support-agent:1",
+ "dataset: .agentops/data/smoke.jsonl",
+ "telemetry_imports:",
+ " - name: prod",
+ " target: log-analytics",
+ " workspace_id: workspace",
+ ]
+ ),
+ encoding="utf-8",
+ )
+ monkeypatch.setattr(
+ "agentops.services.telemetry_import.validate_telemetry_import",
+ lambda _item: [],
+ )
+
+ result = runner.invoke(app, ["telemetry", "validate", "prod", "--config", str(config)])
+
+ assert result.exit_code == 0, result.output
+ assert "prod" in result.output
+ assert "valid" in result.output
+
+
+def test_telemetry_preview_prints_service_preview(tmp_path, monkeypatch) -> None:
+ config = tmp_path / "agentops.yaml"
+ config.write_text(
+ "version: 1\n"
+ "agent: support-agent:1\n"
+ "dataset: .agentops/data/smoke.jsonl\n"
+ "telemetry_imports:\n"
+ " - name: prod\n"
+ " target: log-analytics\n"
+ " workspace_id: workspace\n",
+ encoding="utf-8",
+ )
+
+ def fake_preview(item, *, rows=None, apply=False):
+ return TelemetryImportPreview(
+ config=item,
+ output_path=tmp_path / "prod.jsonl",
+ manifest_path=tmp_path / "prod-manifest.json",
+ rows=[{"input": "hello", "response": "world"}],
+ )
+
+ monkeypatch.setattr(
+ "agentops.services.telemetry_import.preview_telemetry_import",
+ fake_preview,
+ )
+
+ result = runner.invoke(
+ app,
+ ["telemetry", "preview", "prod", "--rows", "1", "--config", str(config)],
+ )
+
+ assert result.exit_code == 0, result.output
+ assert "AgentOps telemetry import" in result.output
+ assert "hello" in result.output
+
+
+def test_telemetry_import_requires_apply_to_write(tmp_path, monkeypatch) -> None:
+ config = tmp_path / "agentops.yaml"
+ config.write_text(
+ "version: 1\n"
+ "agent: support-agent:1\n"
+ "dataset: .agentops/data/smoke.jsonl\n"
+ "telemetry_imports:\n"
+ " - name: prod\n"
+ " target: log-analytics\n"
+ " workspace_id: workspace\n",
+ encoding="utf-8",
+ )
+ calls = []
+
+ def fake_preview(item, *, rows=None, apply=False):
+ calls.append(apply)
+ return TelemetryImportPreview(
+ config=item,
+ output_path=tmp_path / "prod.jsonl",
+ manifest_path=tmp_path / "prod-manifest.json",
+ rows=[],
+ )
+
+ monkeypatch.setattr(
+ "agentops.services.telemetry_import.preview_telemetry_import",
+ fake_preview,
+ )
+
+ result = runner.invoke(app, ["telemetry", "import", "prod", "--config", str(config)])
+
+ assert result.exit_code == 0, result.output
+ assert calls == [False]
+ assert "Dry run only" in result.output
diff --git a/tests/unit/test_http_response_fields.py b/tests/unit/test_http_response_fields.py
new file mode 100644
index 00000000..abbdb324
--- /dev/null
+++ b/tests/unit/test_http_response_fields.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+from agentops.core.agentops_config import AgentOpsConfig, classify_agent
+from agentops.core.evaluators import EvaluatorPreset
+from agentops.pipeline import invocations, orchestrator, runtime
+
+
+def test_http_json_captures_named_response_fields(monkeypatch) -> None:
+ cfg = AgentOpsConfig(
+ version=1,
+ agent="https://example.test/chat",
+ dataset="./qa.jsonl",
+ protocol="http-json",
+ request_field="question",
+ response_fields={
+ "response": "output.answer",
+ "context": "output.context",
+ "citations": "output.citations",
+ },
+ )
+ target = classify_agent(cfg.agent, cfg.protocol)
+
+ def fake_request_json(**_kwargs):
+ return {
+ "output": {
+ "answer": "Use the reset page.",
+ "context": ["Password reset article"],
+ "citations": ["password.md"],
+ }
+ }
+
+ monkeypatch.setattr(invocations, "_http_request_json", fake_request_json)
+
+ result = invocations.invoke(
+ target,
+ cfg,
+ {"input": "How do I reset my password?"},
+ timeout=1,
+ )
+
+ assert result.response == "Use the reset page."
+ assert result.metadata["response_fields"] == {
+ "response": "Use the reset page.",
+ "context": ["Password reset article"],
+ "citations": ["password.md"],
+ }
+
+
+def test_response_fields_are_available_to_evaluator_mapping(monkeypatch) -> None:
+ captured: dict[str, object] = {}
+
+ def fake_evaluator(**kwargs):
+ captured.update(kwargs)
+ return {"score": 5}
+
+ cfg = AgentOpsConfig(
+ version=1,
+ agent="https://example.test/chat",
+ dataset="./qa.jsonl",
+ )
+ target = classify_agent(cfg.agent, cfg.protocol)
+ monkeypatch.setattr(
+ orchestrator.invocations,
+ "invoke",
+ lambda *_args, **_kwargs: invocations.InvocationResult(
+ response="Use the reset page.",
+ latency_seconds=0.25,
+ metadata={
+ "response_fields": {
+ "response": "Use the reset page.",
+ "context": ["Password reset article"],
+ }
+ },
+ ),
+ )
+ evaluator = runtime.EvaluatorRuntime(
+ preset=EvaluatorPreset(
+ name="groundedness",
+ class_name="GroundednessEvaluator",
+ score_key="groundedness",
+ input_mapping={
+ "response": "$prediction",
+ "context": "$response.context",
+ },
+ ),
+ callable=fake_evaluator,
+ )
+
+ row = orchestrator._evaluate_row(
+ row={"input": "question", "expected": "answer"},
+ index=0,
+ total=1,
+ target=target,
+ config=cfg,
+ evaluators=[evaluator],
+ timeout=1,
+ progress=lambda _msg: None,
+ rules_by_metric={},
+ )
+
+ assert row.response == "Use the reset page."
+ assert row.context == '["Password reset article"]'
+ assert captured == {
+ "response": "Use the reset page.",
+ "context": ["Password reset article"],
+ }
diff --git a/tests/unit/test_runtime_conversation.py b/tests/unit/test_runtime_conversation.py
index 605d2b54..a983e3fe 100644
--- a/tests/unit/test_runtime_conversation.py
+++ b/tests/unit/test_runtime_conversation.py
@@ -9,6 +9,7 @@
from __future__ import annotations
from agentops.pipeline.runtime import _build_conversation_messages
+from agentops.pipeline import runtime
def test_builds_text_only_conversation_when_no_tool_calls() -> None:
@@ -130,3 +131,27 @@ def test_skips_calls_without_a_name() -> None:
# Only the named call survives, plus the final assistant text.
assert len(out["response"]) == 2
assert out["response"][0]["content"][0]["name"] == "f"
+
+
+def test_resolves_retrieval_and_telemetry_placeholders() -> None:
+ resolved = runtime._resolve_kwargs(
+ {
+ "context": "$retrieved_context",
+ "items": "$retrieved_context_items",
+ "trace": "$telemetry.trace_id",
+ "json_text": "$response.raw",
+ },
+ row={
+ "input": "q",
+ "retrieved_context": "doc text",
+ "retrieved_context_items": [{"id": "doc1"}],
+ "telemetry": {"trace_id": "trace-123"},
+ "response": {"raw": "nested response"},
+ },
+ response="answer",
+ )
+
+ assert resolved["context"] == "doc text"
+ assert resolved["items"] == [{"id": "doc1"}]
+ assert resolved["trace"] == "trace-123"
+ assert resolved["json_text"] == "nested response"
diff --git a/tests/unit/test_runtime_dataset_response_source.py b/tests/unit/test_runtime_dataset_response_source.py
new file mode 100644
index 00000000..1680e4a2
--- /dev/null
+++ b/tests/unit/test_runtime_dataset_response_source.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+from agentops.core.agentops_config import AgentOpsConfig, classify_agent
+from agentops.core.evaluators import EvaluatorPreset
+from agentops.pipeline import orchestrator, runtime
+
+
+def test_dataset_response_source_does_not_invoke_target(monkeypatch) -> None:
+ config = AgentOpsConfig(
+ version=1,
+ agent="https://example.test/chat",
+ dataset="./qa.jsonl",
+ response_source="dataset",
+ )
+ target = classify_agent(config.agent, config.protocol)
+ latency = EvaluatorPreset(
+ name="avg_latency_seconds",
+ class_name="_latency",
+ score_key="avg_latency_seconds",
+ input_mapping={},
+ )
+
+ def fail_invoke(*args, **kwargs):
+ raise AssertionError("target should not be invoked")
+
+ monkeypatch.setattr(orchestrator.invocations, "invoke", fail_invoke)
+
+ row = orchestrator._evaluate_row(
+ row={"input": "hello", "response": "cached answer", "expected": "cached answer"},
+ index=0,
+ total=1,
+ target=target,
+ config=config,
+ evaluators=[runtime.load_evaluator(latency)],
+ timeout=1,
+ progress=lambda _msg: None,
+ rules_by_metric={},
+ )
+
+ assert row.error is None
+ assert row.response == "cached answer"
+ assert row.latency_seconds == 0.0
+ assert row.metrics[0].name == "avg_latency_seconds"
+ assert row.metrics[0].value == 0.0
+
+
+def test_dataset_response_source_accepts_prediction_field() -> None:
+ config = AgentOpsConfig(
+ version=1,
+ agent="https://example.test/chat",
+ dataset="./qa.jsonl",
+ response_source="dataset",
+ )
+ target = classify_agent(config.agent, config.protocol)
+ latency = EvaluatorPreset(
+ name="avg_latency_seconds",
+ class_name="_latency",
+ score_key="avg_latency_seconds",
+ input_mapping={},
+ )
+
+ row = orchestrator._evaluate_row(
+ row={"input": "hello", "prediction": "predicted answer"},
+ index=0,
+ total=1,
+ target=target,
+ config=config,
+ evaluators=[runtime.load_evaluator(latency)],
+ timeout=1,
+ progress=lambda _msg: None,
+ rules_by_metric={},
+ )
+
+ assert row.response == "predicted answer"
diff --git a/tests/unit/test_telemetry_import.py b/tests/unit/test_telemetry_import.py
new file mode 100644
index 00000000..aa07010f
--- /dev/null
+++ b/tests/unit/test_telemetry_import.py
@@ -0,0 +1,153 @@
+from __future__ import annotations
+
+import builtins
+import json
+
+import pytest
+
+from agentops.core.agentops_config import AgentOpsConfig
+from agentops.services.telemetry_import import (
+ TelemetryImportError,
+ build_telemetry_kql,
+ find_telemetry_import,
+ query_azure_monitor,
+ transform_telemetry_rows,
+ write_telemetry_import,
+)
+
+
+def _config(**overrides):
+ data = {
+ "version": 1,
+ "agent": "support-agent:1",
+ "dataset": ".agentops/data/smoke.jsonl",
+ "telemetry_imports": [
+ {
+ "name": "prod",
+ "target": "application-insights",
+ "resource_id": "$APPINSIGHTS_RESOURCE_ID",
+ "fields": {
+ "input": "customDimensions.question",
+ "response": "customDimensions.answer",
+ "context": "customDimensions.context",
+ },
+ "output": {"path": ".agentops/data/prod.jsonl"},
+ **overrides,
+ }
+ ],
+ }
+ return AgentOpsConfig.model_validate(data).telemetry_imports[0]
+
+
+def test_transform_rows_dedupes_redacts_and_writes_manifest(tmp_path) -> None:
+ cfg = _config(
+ output={"path": str(tmp_path / "prod.jsonl")},
+ privacy={"redact_fields": ["token"], "max_field_length": 100, "include_raw": True},
+ )
+ raw = [
+ {
+ "operation_Id": "trace-1",
+ "id": "turn-1",
+ "customDimensions": {
+ "question": "How do I reset my password?",
+ "answer": "Open account settings.",
+ "context": "Reset article",
+ "token": "secret-token",
+ },
+ },
+ {
+ "operation_Id": "trace-1",
+ "id": "turn-1",
+ "customDimensions": {
+ "question": "How do I reset my password?",
+ "answer": "Open account settings.",
+ },
+ },
+ {"customDimensions": {"question": "missing response"}},
+ ]
+
+ preview = transform_telemetry_rows(cfg, raw)
+ write_telemetry_import(preview)
+
+ assert len(preview.rows) == 1
+ assert preview.deduped == 1
+ assert preview.skipped == 1
+ row = preview.rows[0]
+ assert row["input"] == "How do I reset my password?"
+ assert row["response"] == "Open account settings."
+ assert row["expected"] == "Open account settings."
+ assert row["context"] == "Reset article"
+ assert row["telemetry"]["trace_id"] == "trace-1"
+ assert row["raw"]["customDimensions"]["token"] == "[redacted]"
+ assert (tmp_path / "prod.jsonl").exists()
+ manifest = json.loads((tmp_path / "prod-manifest.json").read_text(encoding="utf-8"))
+ assert manifest["rows"] == 1
+ assert manifest["deduped"] == 1
+
+
+def test_build_kql_uses_safe_generated_filters() -> None:
+ cfg = _config(filters={"customDimensions.agent": ["support", "sales"]}, max_rows=1000)
+
+ kql = build_telemetry_kql(cfg, rows=5)
+
+ assert "union isfuzzy=true requests, dependencies, traces" in kql
+ assert "| extend timestamp = coalesce(" in kql
+ assert "column_ifexists('timestamp', datetime(null))" in kql
+ assert "column_ifexists('TimeGenerated', datetime(null))" in kql
+ assert "coalesce(timestamp, TimeGenerated)" not in kql
+ assert "ago(7d)" in kql
+ assert (
+ "tostring(column_ifexists('customDimensions', dynamic({}))['agent']) "
+ "in ('support', 'sales')"
+ ) in kql
+ assert "operation_Id = column_ifexists('operation_Id', '')" in kql
+ assert "TimeGenerated =" not in kql
+ assert "| order by timestamp desc" in kql
+ assert "take 5" in kql
+
+
+def test_build_kql_guards_plain_filter_columns() -> None:
+ cfg = _config(filters={"name": "agent.response"})
+
+ kql = build_telemetry_kql(cfg, rows=10)
+
+ assert "tostring(column_ifexists('name', '')) == 'agent.response'" in kql
+ assert "tostring(name)" not in kql
+
+
+def test_build_kql_rejects_unsafe_filter_field() -> None:
+ cfg = _config(filters={"name); drop table traces; //": "x"})
+
+ with pytest.raises(TelemetryImportError, match="unsafe"):
+ build_telemetry_kql(cfg)
+
+
+def test_find_telemetry_import_reports_available_names() -> None:
+ cfg = AgentOpsConfig.model_validate(
+ {
+ "version": 1,
+ "agent": "support-agent:1",
+ "dataset": ".agentops/data/smoke.jsonl",
+ "telemetry_imports": [
+ {"name": "prod", "target": "log-analytics", "workspace_id": "workspace"}
+ ],
+ }
+ )
+
+ with pytest.raises(TelemetryImportError, match="prod"):
+ find_telemetry_import(cfg, "missing")
+
+
+def test_query_azure_monitor_reports_missing_sdk(monkeypatch) -> None:
+ cfg = _config()
+ original_import = builtins.__import__
+
+ def fake_import(name, *args, **kwargs):
+ if name == "azure.identity":
+ raise ImportError("no azure")
+ return original_import(name, *args, **kwargs)
+
+ monkeypatch.setattr(builtins, "__import__", fake_import)
+
+ with pytest.raises(TelemetryImportError, match="azure-identity"):
+ query_azure_monitor(cfg, rows=1)
diff --git a/uv.lock b/uv.lock
index dabe39de..da654d2b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -16,6 +16,7 @@ dependencies = [
{ name = "azure-ai-projects" },
{ name = "azure-identity" },
{ name = "azure-monitor-opentelemetry" },
+ { name = "click" },
{ name = "pandas" },
{ name = "pydantic" },
{ name = "ruamel-yaml" },
@@ -64,6 +65,7 @@ requires-dist = [
{ name = "azure-monitor-opentelemetry", specifier = ">=1.6,<2.0" },
{ name = "azure-monitor-opentelemetry", marker = "extra == 'agent'", specifier = ">=1.6,<2.0" },
{ name = "azure-monitor-query", marker = "extra == 'agent'", specifier = ">=1.3,<3.0" },
+ { name = "click", specifier = ">=8.1,<9" },
{ name = "cryptography", marker = "extra == 'agent'", specifier = ">=42" },
{ name = "fastapi", marker = "extra == 'agent'", specifier = ">=0.110,<1.0" },
{ name = "httpx", marker = "extra == 'agent'", specifier = ">=0.27,<1.0" },
@@ -100,7 +102,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.14.0"
+version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -112,108 +114,108 @@ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" },
- { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" },
- { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" },
- { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" },
- { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" },
- { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" },
- { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" },
- { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" },
- { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" },
- { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" },
- { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" },
- { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" },
- { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" },
- { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" },
- { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" },
- { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" },
- { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" },
- { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" },
- { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" },
- { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" },
- { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" },
- { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" },
- { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" },
- { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" },
- { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" },
- { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" },
- { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" },
- { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" },
- { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" },
- { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" },
- { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" },
- { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" },
- { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" },
- { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" },
- { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" },
- { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" },
- { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" },
- { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" },
- { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" },
- { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" },
- { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" },
- { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" },
- { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" },
- { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" },
- { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" },
- { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" },
- { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" },
- { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" },
- { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" },
- { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" },
- { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" },
- { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" },
- { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" },
- { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" },
- { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" },
- { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" },
- { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" },
- { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" },
- { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" },
- { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" },
- { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" },
- { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" },
- { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" },
- { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" },
- { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" },
- { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" },
- { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" },
- { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" },
- { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" },
- { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" },
- { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" },
- { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" },
- { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" },
- { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" },
- { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" },
- { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" },
- { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" },
- { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" },
- { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" },
- { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" },
- { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" },
- { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" },
- { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" },
- { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" },
- { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" },
- { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" },
- { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" },
- { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" },
- { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" },
- { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" },
- { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" },
- { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" },
- { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" },
- { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" },
- { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" },
- { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" },
- { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" },
+ { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" },
+ { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" },
+ { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
+ { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
+ { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
+ { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
+ { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
+ { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
+ { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
+ { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
+ { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
+ { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
+ { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
+ { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
+ { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
+ { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
+ { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
+ { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
+ { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
+ { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
]
[[package]]
@@ -798,61 +800,61 @@ toml = [
[[package]]
name = "cryptography"
-version = "46.0.7"
+version = "48.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
- { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
- { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
- { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
- { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
- { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
- { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
- { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
- { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
- { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
- { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
- { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
- { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
- { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
- { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
- { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
- { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
- { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
- { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
- { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
- { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
- { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
- { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
- { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
- { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
- { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
- { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
- { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
- { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
- { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
- { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
- { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
- { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
- { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
- { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
- { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
- { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
- { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
- { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
- { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
- { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
- { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
+ { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
+ { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
+ { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
+ { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
+ { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
+ { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
+ { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
]
[[package]]
@@ -1103,11 +1105,11 @@ wheels = [
[[package]]
name = "idna"
-version = "3.15"
+version = "3.11"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
@@ -1477,16 +1479,16 @@ wheels = [
[[package]]
name = "msal"
-version = "1.35.1"
+version = "1.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/99/d840198ecf6e8057bbc937f129ae940404485d736cda73253bbff9537f01/msal-1.37.0.tar.gz", hash = "sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec", size = 182444, upload-time = "2026-05-29T19:49:05.561Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" },
+ { url = "https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl", hash = "sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d", size = 123725, upload-time = "2026-05-29T19:49:04.335Z" },
]
[[package]]
@@ -2539,11 +2541,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.29"
+version = "0.0.31"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" },
]
[[package]]
@@ -3001,15 +3003,15 @@ wheels = [
[[package]]
name = "starlette"
-version = "1.2.0"
+version = "1.3.1"
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/c5/bf/616a066c2760f6c2b1ae3437cc28149734d069fbb46511712beae118a68c/starlette-1.2.0.tar.gz", hash = "sha256:3c5a6b23fff42492914e93890bb80cbfea72dbf37de268eec06185d62a4ca553", size = 2668923, upload-time = "2026-05-28T11:42:50.568Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/85/492183764d5d01d4514be3730fdb8e228a80605783099551c51627578b5d/starlette-1.2.0-py3-none-any.whl", hash = "sha256:36e0c76ac59157e75dc4b3bdeafba97fb04eaf1878045f15dbef666a6f092ed7", size = 73213, upload-time = "2026-05-28T11:42:48.801Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
]
[[package]]