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]]