diff --git a/.changeset/testable-docs-snippets.md b/.changeset/testable-docs-snippets.md new file mode 100644 index 00000000..864c3cc6 --- /dev/null +++ b/.changeset/testable-docs-snippets.md @@ -0,0 +1,28 @@ +--- +"adcontextprotocol": minor +--- + +Add testable documentation infrastructure and improve library discoverability + +**Library Discoverability:** +- Added prominent "Client Libraries" section to intro.mdx with NPM badge and installation links +- Updated README.md with NPM package badge and client library installation instructions +- Documented Python client development status (in development, use MCP SDK directly) +- Added links to NPM package, PyPI (future), and GitHub repositories + +**Documentation Snippet Testing:** +- Created comprehensive snippet validation test suite (`tests/snippet-validation.test.js`) +- Extracts code blocks from all documentation files (.md and .mdx) +- Tests JavaScript, TypeScript, Python, and Bash (curl) examples +- Snippets marked with `test=true` or `testable` are automatically validated +- Integration with test suite via `npm run test:snippets` and `npm run test:all` +- Added contributor guide for writing testable documentation snippets + +**What this enables:** +- Documentation examples stay synchronized with protocol changes +- Broken examples are caught in CI before merging +- Contributors can confidently update examples knowing they'll be tested +- Users can trust that documentation code actually works + +**For contributors:** +See `docs/contributing/testable-snippets.md` for how to write testable documentation examples. diff --git a/.gitguardian.yaml b/.gitguardian.yaml new file mode 100644 index 00000000..a4dfc247 --- /dev/null +++ b/.gitguardian.yaml @@ -0,0 +1,13 @@ +version: 2 + +# GitGuardian configuration for AdCP repository +# This file configures what GitGuardian should scan and what to ignore + +# Ignore test tokens that are documented and meant to be public +matches-ignore: + - name: Test Agent Authentication Token + match: 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ + comment: | + This is a documented test token for the public test agent at + https://test-agent.adcontextprotocol.org/mcp. It's intentionally + included in documentation examples and has no production access. diff --git a/.github/workflows/check-testable-snippets.yml b/.github/workflows/check-testable-snippets.yml new file mode 100644 index 00000000..7dea6c92 --- /dev/null +++ b/.github/workflows/check-testable-snippets.yml @@ -0,0 +1,32 @@ +name: Check Testable Snippets + +on: + pull_request: + paths: + - 'docs/**/*.md' + - 'docs/**/*.mdx' + +jobs: + check-snippets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history for diff + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Check for untested code snippets + run: | + git fetch origin ${{ github.base_ref }} + git diff origin/${{ github.base_ref }}...HEAD --name-only | grep -E '\.(md|mdx)$' > changed_files.txt || true + + if [ -s changed_files.txt ]; then + echo "πŸ“‹ Checking documentation changes for testable snippets..." + node scripts/check-testable-snippets.js + else + echo "βœ“ No documentation files changed" + fi diff --git a/CLAUDE.md b/CLAUDE.md index 5a65253c..8065f413 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,27 @@ This guide helps AI assistants understand the AdCP project structure and maintai The Advertising Context Protocol (AdCP) is an open standard for AI-powered advertising workflows. It provides a unified interface for media buying across diverse advertising platforms. +## Documentation Framework + +**CRITICAL**: This project uses TWO documentation systems: + +1. **Mintlify** - Primary documentation platform + - All documentation in `docs/` directory + - Markdown/MDX files served by Mintlify + - Uses Mintlify-specific components (``, not Docusaurus ``) + - Run with: `mintlify dev` (should use conductor port, not 3000) + +2. **Docusaurus** - Legacy/backwards compatibility only + - Used for homepage + - Used to serve JSON schemas at `/schemas/` endpoints + - Will be migrated away from eventually + - DO NOT use Docusaurus components in documentation files + +**When editing documentation:** +- βœ… Use Mintlify `` for multi-language examples +- ❌ DO NOT use Docusaurus `import Tabs from '@theme/Tabs'` +- ❌ DO NOT use `` or `` components + ## Documentation Standards ### Protocol Specification vs Implementation @@ -42,6 +63,79 @@ Implementation details can be mentioned as: - Write for an audience implementing the protocol, not using a specific implementation - Keep examples generic and illustrative +### Testable Documentation + +**IMPORTANT**: All code examples in documentation should be testable when possible. + +**How to mark pages as testable**: + +Add `testable: true` to the frontmatter of pages where all code examples should be tested: + +```markdown +--- +title: get_products +sidebar_position: 1 +testable: true +--- + +# get_products + +...code examples here (no test=true needed in individual blocks)... +``` + +**Key principles**: +1. **Page-level flag** - Use `testable: true` in frontmatter to mark entire page as testable +2. **Tab titles** - The text after the language becomes the tab title (e.g., "JavaScript", "Python", "CLI") +3. **Complete examples** - All code on testable pages must be complete and runnable +4. **Use test credentials** - Use the public test agent credentials in examples + +**Supported languages**: +- `javascript` / `typescript` - Runs with Node.js ESM modules +- `python` - Runs with Python 3.11+ +- `bash` - Supports `curl`, `npx`, and `uvx` commands + +**What gets tested**: +- All code blocks on pages with `testable: true` frontmatter +- Code executes without errors +- API calls succeed (or fail as expected) +- Output matches expectations + +**When NOT to mark page as testable**: +- Pages with incomplete code fragments +- Conceptual examples or pseudocode +- Browser-only code examples +- Code requiring user interaction +- Mixed testable and non-testable examples (use separate pages) + +**Example testable page**: + +```markdown +--- +title: get_products +testable: true +--- + +# get_products + + + +\`\`\`javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; +const client = new ADCPMultiAgentClient([...]); +\`\`\` + +\`\`\`python Python +from adcp import ADCPMultiAgentClient +\`\`\` + + +``` + +**Running tests**: +```bash +node tests/snippet-validation.test.js +``` + ## JSON Schema Maintenance ### Schema-Documentation Consistency diff --git a/README.md b/README.md index 3f0ed573..80f4b169 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![GitHub stars](https://img.shields.io/github/stars/adcontextprotocol/adcp?style=social)](https://github.com/adcontextprotocol/adcp) [![Documentation](https://img.shields.io/badge/docs-adcontextprotocol.org-blue)](https://adcontextprotocol.org) +[![npm version](https://img.shields.io/npm/v/@adcp/client)](https://www.npmjs.com/package/@adcp/client) [![MCP Compatible](https://img.shields.io/badge/MCP-compatible-green)](https://modelcontextprotocol.io) > **AdCP revolutionizes advertising automation by providing a single, AI-powered protocol that works across all major advertising platforms.** @@ -53,6 +54,22 @@ Execute and optimize media buys programmatically across platforms. ## Quick Start +### Install Client Libraries + +#### JavaScript/TypeScript +```bash +npm install @adcp/client +``` +- **NPM Package**: [@adcp/client](https://www.npmjs.com/package/@adcp/client) +- **GitHub**: [adcp-client](https://github.com/adcontextprotocol/adcp-client) + +#### Python +```bash +pip install adcp +``` +- **PyPI Package**: [adcp](https://pypi.org/project/adcp/) +- **GitHub**: [adcp-python](https://github.com/adcontextprotocol/adcp-python) + ### For Platform Providers Implement AdCP to enable AI-powered workflows for your customers: diff --git a/docs/contributing/testable-examples-demo.md b/docs/contributing/testable-examples-demo.md new file mode 100644 index 00000000..74096eb2 --- /dev/null +++ b/docs/contributing/testable-examples-demo.md @@ -0,0 +1,84 @@ +--- +testable: true +--- + +# Testable Documentation Examples + +This page demonstrates the testable documentation feature with complete, working code examples that execute against the live test agent. + +## JavaScript Example + +### List Creative Formats + +```javascript +import { testAgent } from '@adcp/client/testing'; + +const result = await testAgent.listCreativeFormats({}); + +console.log(`βœ“ Found ${result.data?.formats?.length || 0} creative formats`); +``` + +## Python Example + +### List Creative Formats + +```python +import asyncio +from adcp import test_agent + +async def list_formats(): + result = await test_agent.simple.list_creative_formats() + print(f"βœ“ Found {len(result.formats)} supported creative formats") + +asyncio.run(list_formats()) +``` + +## CLI Example + +### Using uvx (Python CLI) + +```bash +uvx adcp \ + https://test-agent.adcontextprotocol.org/mcp \ + list_creative_formats \ + '{}' \ + --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ +``` + +## How Testable Documentation Works + +When `testable: true` is set in the frontmatter, ALL code blocks on this page are extracted and executed during testing. + +### Running Tests + +```bash +# Run all tests including snippet validation +npm run test:all +``` + +### Requirements for Testable Pages + +Every code block must: +- Be complete and self-contained +- Import all required dependencies +- Execute without errors +- Produce output confirming success + +### When to Mark Pages as Testable + +Mark a page `testable: true` ONLY when: +- ALL code blocks are complete working examples +- No code fragments or incomplete snippets +- All examples use test agent credentials +- Dependencies are installed (`@adcp/client`, `adcp`) + +### When NOT to Mark Pages as Testable + +Do NOT mark pages testable that contain: +- Code fragments showing patterns +- Incomplete examples +- Conceptual pseudocode +- Examples requiring production credentials +- Mixed testable and non-testable content + +See [Testable Snippets Guide](./testable-snippets.md) for complete documentation. diff --git a/docs/contributing/testable-snippets.md b/docs/contributing/testable-snippets.md new file mode 100644 index 00000000..9b322608 --- /dev/null +++ b/docs/contributing/testable-snippets.md @@ -0,0 +1,333 @@ +# Writing Testable Documentation Snippets + +This guide explains how to write code examples in AdCP documentation that are automatically tested for correctness. + +## Why Test Documentation Snippets? + +Automated testing of documentation examples ensures: +- Examples stay up-to-date with the latest API +- Code snippets actually work as shown +- Breaking changes are caught immediately +- Users can trust the documentation + +**Important**: The test infrastructure validates code blocks **directly in the documentation files** (`.md` and `.mdx`). When you mark a page with `testable: true` in the frontmatter, ALL code blocks on that page are extracted and executed. + +## Marking Pages for Testing + +To mark an entire page as testable, add `testable: true` to the frontmatter: + +```markdown +--- +title: get_products +sidebar_position: 1 +testable: true +--- + +# get_products + +...all code examples here will be tested... +``` + +**Key principle**: Pages should be EITHER fully testable OR not testable at all. We don't support partially testable pages (mixing testable and non-testable code blocks on the same page). + +### Example Code Blocks + +Once a page is marked `testable: true`, all code blocks are executed: + +````markdown +```javascript +import { testAgent } from '@adcp/client/testing'; + +const products = await testAgent.getProducts({ + brief: 'Premium athletic footwear with innovative cushioning', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + } +}); + +console.log(`Found ${products.products.length} products`); +``` +```` + +### Using Test Helpers + +For simpler examples, use the built-in test helpers from client libraries: + +**JavaScript:** +```javascript +import { testAgent, testAgentNoAuth } from '@adcp/client/testing'; + +// Authenticated access +const fullCatalog = await testAgent.getProducts({ + brief: 'Premium CTV inventory' +}); + +// Unauthenticated access +const publicCatalog = await testAgentNoAuth.getProducts({ + brief: 'Premium CTV inventory' +}); +``` + +**Python:** +```python +import asyncio +from adcp import test_agent, test_agent_no_auth + +async def example(): + # Authenticated access + full_catalog = await test_agent.simple.get_products( + brief='Premium CTV inventory' + ) + + # Unauthenticated access + public_catalog = await test_agent_no_auth.simple.get_products( + brief='Premium CTV inventory' + ) + +asyncio.run(example()) +``` + +## Best Practices + +### 1. Use Test Agent Credentials + +Always use the public test agent for examples: + +- **Test Agent URL**: `https://test-agent.adcontextprotocol.org` +- **MCP Token**: `1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ` +- **A2A Token**: `L4UCklW_V_40eTdWuQYF6HD5GWeKkgV8U6xxK-jwNO8` + +### 2. Make Examples Self-Contained + +Each testable snippet should: +- Import all required dependencies +- Initialize connections +- Execute a complete operation +- Produce visible output (console.log, etc.) + +**Good Example:** +```javascript +// Example of a complete, testable snippet +import { AdcpClient } from '@adcp/client'; + +const client = new AdcpClient({ + agentUrl: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + bearerToken: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' +}); + +const products = await client.getProducts({ + promoted_offering: 'Nike Air Max 2024' +}); + +console.log('Success:', products.products.length > 0); +``` + +**Bad Example (incomplete):** +```javascript +// Don't mark this for testing - it's incomplete +const products = await client.getProducts({ + promoted_offering: 'Nike Air Max 2024' +}); +``` + +### 3. Use Dry Run Mode + +When demonstrating operations that modify state (create, update, delete), use dry run mode: + +```javascript +// Example showing dry run mode usage +const mediaBuy = await client.createMediaBuy({ + product_id: 'prod_123', + budget: 10000, + start_date: '2025-11-01', + end_date: '2025-11-30' +}, { + dryRun: true // No actual campaign created +}); + +console.log('Dry run successful'); +``` + +### 4. Handle Async Operations + +JavaScript/TypeScript examples should use `await` or `.then()`: + +```javascript +// Using await (recommended) +const products = await client.getProducts({...}); + +// Or using .then() +client.getProducts({...}).then(products => { + console.log('Products:', products.products.length); +}); +``` + +### 5. Keep Examples Focused + +Each testable snippet should demonstrate ONE concept: + +```javascript +// Good: Demonstrates authentication +import { AdcpClient } from '@adcp/client'; + +const client = new AdcpClient({ + agentUrl: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + bearerToken: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' +}); + +console.log('Authenticated:', client.isAuthenticated); +``` + +## When NOT to Mark Pages as Testable + +Some documentation pages should NOT have `testable: true`: + +### 1. Pages with Pseudo-code or Conceptual Examples + +If your page includes conceptual examples that aren't meant to execute: + +```javascript +// Conceptual workflow - not actual code +const result = await magicFunction(); // βœ— Not a real function +``` + +### 2. Pages with Incomplete Code Fragments + +Pages showing partial code snippets for illustration: + +```javascript +// Incomplete fragment showing field structure +budget: 10000, +start_date: '2025-11-01' +``` + +### 3. Pages with Configuration/Schema Examples + +Documentation showing JSON schemas or configuration structures: + +```json +{ + "product_id": "example", + "name": "Example Product" +} +``` + +### 4. Pages with Response Examples + +Pages showing example API responses (not requests): + +```json +{ + "products": [ + {"product_id": "prod_123", "name": "Premium Display"} + ] +} +``` + +### 5. Pages with Mixed Testable and Non-Testable Code + +If your page has SOME runnable code but SOME conceptual code, split into separate pages: +- One page marked `testable: true` with complete, runnable examples +- Another page without the flag for conceptual/partial examples + +**Remember**: Every code block on a testable page will be executed. If any block can't run, don't mark the page as testable. + +## Running Snippet Tests + +### Locally + +Test all documentation snippets: + +```bash +npm test +``` + +Or specifically run the snippet tests: + +```bash +node tests/snippet-validation.test.js +``` + +This will: +1. Scan all `.md` and `.mdx` files in `docs/` +2. Find pages with `testable: true` in frontmatter +3. Extract ALL code blocks from those pages +4. Execute each snippet and report results +5. Exit with error if any tests fail + +### In CI/CD + +The full test suite (including snippet tests) can be run with: + +```bash +npm run test:all +``` + +This includes: +- Schema validation +- Example validation +- Snippet validation +- TypeScript type checking + +## Supported Languages + +Currently supported languages for testing: + +- **JavaScript** (`.js`, `javascript`, `js`) +- **TypeScript** (`.ts`, `typescript`, `ts`) - compiled to JS +- **Bash** (`.sh`, `bash`, `shell`) - only `curl` commands +- **Python** (`.py`, `python`) - requires Python 3 installed + +### Limitations + +**Package Dependencies**: Snippets that import external packages (like `@adcp/client` or `adcp`) will only work if: +1. The package is installed in the repository's `node_modules` +2. Or the package is listed in `devDependencies` + +For examples requiring the client library, you have options: +- **Option 1**: Add the library to `devDependencies` so tests can import it +- **Option 2**: Don't mark those snippets as testable; document them as conceptual examples instead +- **Option 3**: Use curl/HTTP examples for testable documentation (no package dependencies) + +## Debugging Failed Tests + +When a snippet test fails: + +1. **Check the error message** - The test output shows which file and line number failed +2. **Run the snippet manually** - Copy the code and run it locally +3. **Verify test agent is accessible** - Check https://test-agent.adcontextprotocol.org +4. **Check dependencies** - Ensure all imports are available +5. **Review the snippet** - Make sure it's self-contained + +Example error output: + +``` +Testing: quickstart.mdx:272 (javascript block #6) + βœ— FAILED + Error: Cannot find module '@adcp/client' +``` + +This indicates the `@adcp/client` package needs to be installed. + +## Contributing Guidelines + +When adding new documentation: + +1. βœ… **DO** mark entire pages as `testable: true` if ALL code blocks are runnable +2. βœ… **DO** use test helpers from client libraries for simpler examples +3. βœ… **DO** test snippets locally before committing (`npm test`) +4. βœ… **DO** keep examples self-contained and complete +5. βœ… **DO** use test agent credentials in examples +6. ❌ **DON'T** mark pages with ANY incomplete fragments as testable +7. ❌ **DON'T** mark pages with pseudo-code as testable +8. ❌ **DON'T** mix testable and non-testable code on the same page +9. ❌ **DON'T** use production credentials in examples + +## Questions? + +- Check existing testable examples in `docs/quickstart.mdx` +- Review the test suite: `tests/snippet-validation.test.js` +- Ask in [Slack Community](https://join.slack.com/t/agenticads/shared_invite/zt-3c5sxvdjk-x0rVmLB3OFHVUp~WutVWZg) diff --git a/docs/intro.mdx b/docs/intro.mdx index 41b07866..7abbc1e1 100644 --- a/docs/intro.mdx +++ b/docs/intro.mdx @@ -162,11 +162,30 @@ Test all AdCP tasks in your browser - no code required. ### πŸ“– [**Quickstart Guide**](/docs/quickstart) Get started in 5 minutes with authentication, testing, and your first request. -### πŸ’» **Install the NPM Client** +### πŸ’» **Client Libraries** + +#### JavaScript/TypeScript +[![npm version](https://img.shields.io/npm/v/@adcp/client)](https://www.npmjs.com/package/@adcp/client) + ```bash npm install @adcp/client ``` +- **NPM**: [@adcp/client](https://www.npmjs.com/package/@adcp/client) +- **GitHub**: [adcp-client](https://github.com/adcontextprotocol/adcp-client) +- **Documentation**: [JavaScript Client Guide](/docs/quickstart#code-examples) + +#### Python +[![PyPI version](https://img.shields.io/pypi/v/adcp)](https://pypi.org/project/adcp/) + +```bash +pip install adcp +``` + +- **PyPI**: [adcp](https://pypi.org/project/adcp/) +- **GitHub**: [adcp-python](https://github.com/adcontextprotocol/adcp-python) +- **Documentation**: [Python Client Guide](/docs/quickstart#code-examples) + ## Example: Natural Language Advertising Instead of navigating multiple platforms, you can now say: diff --git a/docs/media-buy/task-reference/create_media_buy.mdx b/docs/media-buy/task-reference/create_media_buy.mdx index e4806726..e79d0d5c 100644 --- a/docs/media-buy/task-reference/create_media_buy.mdx +++ b/docs/media-buy/task-reference/create_media_buy.mdx @@ -5,1188 +5,587 @@ sidebar_position: 3 # create_media_buy -Create a media buy from selected packages. This task handles the complete workflow including validation, approval if needed, and campaign creation. +Create a media buy from selected products. Handles validation, optional approval workflows, and campaign creation. -**Response Time**: Instant to days (returns `completed`, `working` < 120s, or `submitted` for hours/days) +**Response Time**: Instant to days (status: `completed`, `working` < 120s, or `submitted` for manual review) -**Pricing & Currency**: Each package specifies its own `pricing_option_id`, which determines currency, pricing model (CPM, CPCV, CPP, etc.), and rates. Packages can use different currencies when sellers support itβ€”sellers validate and reject incompatible combinations. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) for details. +**Format Specification Required**: Each package must specify creative formats for placeholder creation and asset requirements. -**Format Specification Required**: Each package must specify the creative formats that will be used. This enables placeholder creation in ad servers and ensures both parties have clear expectations for creative asset requirements. +**Request Schema**: [`/schemas/v1/media-buy/create-media-buy-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/create-media-buy-request.json) +**Response Schema**: [`/schemas/v1/media-buy/create-media-buy-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/create-media-buy-response.json) +## Quick Start -**Request Schema**: [/schemas/v1/media-buy/create-media-buy-request.json](https://adcontextprotocol.org/schemas/v1/media-buy/create-media-buy-request.json) -**Response Schema**: [/schemas/v1/media-buy/create-media-buy-response.json](https://adcontextprotocol.org/schemas/v1/media-buy/create-media-buy-response.json) +Create a media buy from a discovered product: -## Request Parameters - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `buyer_ref` | string | Yes | Buyer's reference identifier for this media buy | -| `packages` | Package[] | Yes | Array of package configurations (see Package Object below) | -| `brand_manifest` | BrandManifestRef | Yes | Brand information manifest serving as the namespace and identity for this media buy. Provides brand context, assets, and product catalog. Can be provided as an inline object or URL reference to a hosted manifest. Can be cached and reused across multiple requests. See [Brand Manifest](/docs/creative/brand-manifest) for details. | -| `promoted_products` | PromotedProducts | No | Products or offerings being promoted in this media buy. Useful for campaign-level reporting, policy compliance, and publisher understanding of what's being advertised. Selects from brand manifest's product catalog using SKUs, tags, categories, or natural language queries. | -| `po_number` | string | No | Purchase order number for tracking | -| `start_time` | string | Yes | Campaign start time: `"asap"` to start as soon as possible, or ISO 8601 date-time for scheduled start | -| `end_time` | string | Yes | Campaign end date/time in ISO 8601 format (UTC unless timezone specified) | -| `reporting_webhook` | ReportingWebhook | No | Optional webhook configuration for automated reporting delivery (see Reporting Webhook Object below) | - -### Package Object - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `buyer_ref` | string | Yes | Buyer's reference identifier for this package | -| `product_id` | string | Yes | Product ID for this package | -| `pricing_option_id` | string | Yes | Pricing option ID from the product's `pricing_options` array - specifies pricing model and currency for this package. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) for details. | -| `format_ids` | FormatID[] | Yes | Array of structured format ID objects that will be used for this package - must be supported by the product | -| `budget` | number | Yes | Budget allocation for this package in the currency specified by the pricing option | -| `pacing` | string | No | Pacing strategy: `"even"` (default), `"asap"`, or `"front_loaded"` | -| `bid_price` | number | No | Bid price for auction-based pricing options (required when `pricing_option.is_fixed` is false) | -| `targeting_overlay` | TargetingOverlay | No | Additional targeting criteria for this package (see Targeting Overlay Object below) | -| `creative_ids` | string[] | No | Creative IDs to assign to this package at creation time (references existing library creatives) | -| `creatives` | CreativeAsset[] | No | Full creative objects to upload and assign to this package at creation time (alternative to creative_ids - creatives will be added to library). Supports both static and generative creatives. Max 100 per package. | - -### Targeting Overlay Object - -**Note**: Targeting overlays should be rare. Most targeting should be expressed in your brief and handled by the publisher through product selection. Use overlays only for geographic restrictions (RCT testing, regulatory compliance) or frequency capping. - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `geo_country_any_of` | string[] | No | Restrict delivery to specific countries (ISO codes). Use for regulatory compliance or RCT testing. | -| `geo_region_any_of` | string[] | No | Restrict delivery to specific regions/states. Use for regulatory compliance or RCT testing. | -| `geo_metro_any_of` | string[] | No | Restrict delivery to specific metro areas (DMA codes). Use for regulatory compliance or RCT testing. | -| `geo_postal_code_any_of` | string[] | No | Restrict delivery to specific postal/ZIP codes. Use for regulatory compliance or RCT testing. | -| `frequency_cap` | FrequencyCap | No | Frequency capping settings (see Frequency Cap Object below) | + -### Frequency Cap Object +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `suppress_minutes` | number | Yes | Minutes to suppress after impression (applied at package level) | - -### Reporting Webhook Object - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `url` | string | Yes | Webhook endpoint URL for reporting notifications | -| `auth_type` | string | Yes | Authentication type: `"bearer"`, `"basic"`, or `"none"` | -| `auth_token` | string | No* | Authentication token or credentials (required unless auth_type is "none") | -| `reporting_frequency` | string | Yes | Reporting frequency: `"hourly"`, `"daily"`, or `"monthly"`. Must be supported by all products in the media buy. | -| `requested_metrics` | string[] | No | Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's `available_metrics`. | - -**Publisher Commitment**: When a reporting webhook is configured, the publisher commits to sending **(campaign_duration / reporting_frequency) + 1** webhook notifications: -- One notification per frequency period during the campaign -- One final notification when the campaign completes -- If reporting data is delayed beyond the product's `expected_delay_minutes`, a notification with `"delayed"` status will be sent to avoid appearing as a missed notification - -**Timezone Considerations**: For daily and monthly frequencies, the publisher's reporting timezone (specified in `reporting_capabilities.timezone`) determines when periods begin/end. Ensure alignment between your systems and the publisher's timezone to avoid confusion about reporting period boundaries. - -## Response (Message) - -The response includes a human-readable message that: -- Confirms the media buy was created with budget and targeting details -- Explains next steps and deadlines -- Describes any approval requirements -- Provides implementation details and status updates - -The message is returned differently in each protocol: -- **MCP**: Returned as a `message` field in the JSON response -- **A2A**: Returned as a text part in the artifact - -## Response (Payload) - -```json -{ - "media_buy_id": "string", - "buyer_ref": "string", - "creative_deadline": "string", - "packages": [ - { - "package_id": "string", - "buyer_ref": "string" - } - ] -} -``` - -### Field Descriptions - -- **media_buy_id**: Publisher's unique identifier for the created media buy -- **buyer_ref**: Buyer's reference identifier for this media buy -- **creative_deadline**: ISO 8601 timestamp for creative upload deadline -- **packages**: Array of created packages - - **package_id**: Publisher's unique identifier for the package - - **buyer_ref**: Buyer's reference identifier for the package - -## Protocol-Specific Examples - -The AdCP payload is identical across protocols. Only the request/response wrapper differs. - -### MCP Request -```json -{ - "tool": "create_media_buy", - "arguments": { - "buyer_ref": "nike_q1_campaign_2024", - "packages": [ - { - "buyer_ref": "nike_ctv_sports_package", - "product_id": "ctv_sports_premium", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_30s" - }, - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_15s" - } - ], - "budget": 60000, - "pacing": "even", - "pricing_option_id": "cpm-fixed-sports", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "NY"], - "axe_include_segment": "x8dj3k" - }, - "creative_ids": ["creative_abc123", "creative_def456"] - }, - { - "buyer_ref": "nike_audio_drive_package", - "product_id": "audio_drive_time", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "audio_standard_30s" - } - ], - "budget": 40000, - "pacing": "front_loaded", - "pricing_option_id": "cpm-fixed-audio", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA"], - "axe_exclude_segment": "x9m2p" - } - } - ], - "promoted_offering": "Nike Air Max 2024 - premium running shoes", - "po_number": "PO-2024-Q1-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-03-31T23:59:59Z", - "reporting_webhook": { - "url": "https://buyer.example.com/webhooks/reporting", - "auth_type": "bearer", - "auth_token": "secret_reporting_token_xyz", - "reporting_frequency": "daily", - "requested_metrics": ["impressions", "spend", "video_completions", "completion_rate"] - } +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } -} -``` - -### MCP Response (Synchronous) -```json -{ - "message": "Successfully created $100,000 media buy. Upload creatives by Jan 30. Campaign will run from Feb 1 to Mar 31.", - "status": "completed", - "media_buy_id": "mb_12345", - "buyer_ref": "nike_q1_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - { - "package_id": "pkg_12345_001", - "buyer_ref": "nike_ctv_sports_package" - }, - { - "package_id": "pkg_12345_002", - "buyer_ref": "nike_audio_drive_package" - } - ] -} -``` - -### MCP Response (Partial Success with Errors) -```json -{ - "message": "Media buy created but some packages had issues. Review targeting for best performance.", - "media_buy_id": "mb_12346", - "buyer_ref": "nike_q1_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - { - "package_id": "pkg_12346_001", - "buyer_ref": "nike_ctv_sports_package" - } - ], - "errors": [ - { - "code": "TARGETING_TOO_NARROW", - "message": "Package targeting yielded 0 available impressions", - "field": "packages[1].targeting_overlay", - "suggestion": "Broaden geographic targeting or remove segment exclusions", - "details": { - "requested_budget": 40000, - "available_impressions": 0, - "affected_package": "nike_audio_drive_package" - } - } - ] -} -``` - -### MCP Response (Asynchronous) -```json -{ - "task_id": "task_456", - "status": "working", - "message": "Creating media buy...", - "poll_url": "/tasks/task_456" -} -``` +}]); -### A2A Request +const agent = client.agent('test-agent'); -#### Natural Language Invocation -```javascript -await a2a.send({ - message: { - parts: [{ - kind: "text", - text: "Create a $100K Nike campaign from Feb 1 to Mar 31. Use the CTV sports and audio drive time products we discussed. Split budget 60/40." - }] +// First, discover products +const discovery = await agent.getProducts({ + brief: 'Premium video inventory for athletic footwear', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' } }); -``` -#### Explicit Skill Invocation -```javascript -await a2a.send({ - message: { - parts: [ - { - kind: "text", - text: "Creating Nike Q1 campaign" // Optional context - }, - { - kind: "data", - data: { - skill: "create_media_buy", // Must match skill name in Agent Card - parameters: { - "buyer_ref": "nike_q1_campaign_2024", - "packages": [ - { - "buyer_ref": "nike_ctv_sports_package", - "product_id": "ctv_sports_premium", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_30s" - }, - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_15s" - } - ], - "budget": 60000, - "pacing": "even", - "pricing_option_id": "cpm-fixed-sports", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "NY"], - "axe_include_segment": "x8dj3k" - }, - "creative_ids": ["creative_abc123", "creative_def456"] - }, - { - "buyer_ref": "nike_audio_drive_package", - "product_id": "audio_drive_time", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "audio_standard_30s" - } - ], - "budget": 40000, - "pacing": "front_loaded", - "pricing_option_id": "cpm-fixed-audio", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA"], - "axe_exclude_segment": "x9m2p" - } - } - ], - "promoted_offering": "Nike Air Max 2024 - premium running shoes", - "po_number": "PO-2024-Q1-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-03-31T23:59:59Z" - } - } - } - ] - } +// Create media buy with first product +const product = discovery.products[0]; +const mediaBuy = await agent.createMediaBuy({ + buyer_ref: 'nike-q1-2024', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, + packages: [{ + buyer_ref: 'package-1', + product_id: product.product_id, + pricing_option_id: product.pricing_options[0].pricing_option_id, + format_ids: product.format_ids, + budget: 10000 + }], + start_time: 'asap', + end_time: '2024-12-31T23:59:59Z' }); -``` -### A2A Response (with streaming) -Initial response: -```json -{ - "taskId": "task-mb-001", - "status": { "state": "working" } -} +console.log(`Media buy created: ${mediaBuy.media_buy_id}`); ``` -Then via Server-Sent Events: -``` -data: {"message": "Validating packages..."} -data: {"message": "Checking inventory availability..."} -data: {"message": "Creating campaign in ad server..."} -data: {"status": {"state": "completed"}, "artifacts": [{ - "name": "media_buy_confirmation", - "parts": [ - {"kind": "text", "text": "Successfully created $100,000 media buy. Upload creatives by Jan 30."}, - {"kind": "data", "data": { - "media_buy_id": "mb_12345", - "buyer_ref": "nike_q1_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - {"package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package"}, - {"package_id": "pkg_12345_002", "buyer_ref": "nike_audio_drive_package"} - ] - }} - ] -}]} -``` - -### Key Differences -- **MCP**: May return synchronously or asynchronously with updates via: - - Polling (calling status endpoints) - - Webhooks (push notifications to callback URLs) - - Streaming (WebSockets or SSE) -- **A2A**: Always returns task with updates via: - - Server-Sent Events (SSE) for real-time streaming - - Webhooks (push notifications) for long-running tasks -- **Payload**: The `input` field in A2A contains the exact same structure as MCP's `arguments` - -## Human-in-the-Loop Examples - -### MCP with Manual Approval (Polling Example) - -This example shows polling, but MCP implementations may also support webhooks or streaming for real-time updates. - -**Initial Request:** -```json -{ - "tool": "create_media_buy", - "arguments": { - "buyer_ref": "large_campaign_2024", - "packages": [...], - "promoted_offering": "High-value campaign requiring approval", - "po_number": "PO-2024-LARGE-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-06-30T23:59:59Z" - } -} -``` +```python Python +from adcp import ADCPMultiAgentClient -**Response (Asynchronous):** -```json -{ - "task_id": "task_456", - "status": "working", - "message": "Large budget requires sales team approval. Expected review time: 2-4 hours.", - "context_id": "ctx-mb-456" -} -``` +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]) -**Client checks status (via polling in this example):** -```json -{ - "tool": "create_media_buy_status", - "arguments": { - "context_id": "ctx-mb-456" - } -} -``` +agent = client.agent('test-agent') -**Status Response (Still pending):** -```json -{ - "status": "working", - "message": "Awaiting manual approval. Sales team reviewing. 1 of 2 approvals received.", - "context_id": "ctx-mb-456", - "responsible_party": "publisher", - "estimated_completion": "2024-01-15T16:00:00Z" -} -``` +# First, discover products +discovery = agent.get_products( + brief='Premium video inventory for athletic footwear', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } +) + +# Create media buy with first product +product = discovery['products'][0] +media_buy = agent.create_media_buy( + buyer_ref='nike-q1-2024', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + packages=[{ + 'buyer_ref': 'package-1', + 'product_id': product['product_id'], + 'pricing_option_id': product['pricing_options'][0]['pricing_option_id'], + 'format_ids': product['format_ids'], + 'budget': 10000 + }], + start_time='asap', + end_time='2024-12-31T23:59:59Z' +) -**Status Response (Approved):** -```json -{ - "status": "completed", - "message": "Media buy approved and created. Upload creatives by Jan 30.", - "media_buy_id": "mb_789456", - "buyer_ref": "large_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [...] -} +print(f"Media buy created: {media_buy['media_buy_id']}") ``` -### A2A with Manual Approval (SSE Example) - -A2A can use Server-Sent Events for real-time streaming or webhooks for push notifications. - -**Initial Request with SSE:** -```json -{ - "skill": "create_media_buy", - "input": { - "buyer_ref": "large_campaign_2024", - "packages": [...], - "promoted_offering": "High-value campaign requiring approval", - "po_number": "PO-2024-LARGE-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-06-30T23:59:59Z" - } -} -``` + -**Initial Response:** -```json -{ - "taskId": "task-mb-large-001", - "contextId": "ctx-conversation-xyz", - "status": { - "state": "working", - "message": "Large budget requires sales team approval" - } -} -``` +## Request Parameters -**SSE Updates (Human approval process):** -``` -data: {"message": "Validating campaign requirements..."} -data: {"message": "Budget exceeds auto-approval threshold. Routing to sales team..."} -data: {"message": "Sales team notified. Expected review time: 2-4 hours"} -data: {"message": "First approval received from regional manager"} -data: {"message": "Second approval received from finance team"} -data: {"status": {"state": "completed"}, "artifacts": [{ - "artifactId": "artifact-mb-large-xyz", - "name": "media_buy_confirmation", - "parts": [ - {"kind": "text", "text": "Media buy approved and created successfully. $500,000 campaign scheduled Feb 1 - Jun 30. Upload creatives by Jan 30."}, - {"kind": "data", "data": { - "media_buy_id": "mb_789456", - "buyer_ref": "large_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [...] - }} - ] -}]} -``` +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `buyer_ref` | string | Yes | Buyer's reference identifier | +| `packages` | Package[] | Yes | Package configurations (see below) | +| `brand_manifest` | BrandManifest | Yes | Brand information. See [Brand Manifest](/docs/creative/brand-manifest) | +| `promoted_products` | PromotedProducts | No | Products being promoted (for reporting/policy compliance) | +| `po_number` | string | No | Purchase order number | +| `start_time` | string | Yes | `"asap"` or ISO 8601 date-time | +| `end_time` | string | Yes | ISO 8601 date-time (UTC unless timezone specified) | +| `reporting_webhook` | ReportingWebhook | No | Optional webhook for automated reporting delivery | -### A2A with Webhooks (Long-Running Task) - -**Initial Request with Webhook Configuration:** -```json -{ - "skill": "create_media_buy", - "input": { - "buyer_ref": "large_campaign_2024", - "packages": [...], - "promoted_offering": "High-value campaign requiring approval", - "po_number": "PO-2024-LARGE-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-06-30T23:59:59Z" - }, - "pushNotificationConfig": { - "url": "https://buyer.example.com/webhooks/adcp", - "authType": "bearer", - "authToken": "secret-token-xyz" - } -} -``` +### Package Object -**Initial Response:** -```json -{ - "taskId": "task-mb-webhook-001", - "contextId": "ctx-conversation-xyz", - "status": { - "state": "working", - "message": "Task processing. Updates will be sent to webhook." +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `buyer_ref` | string | Yes | Buyer's package reference | +| `product_id` | string | Yes | Product ID from discovery | +| `pricing_option_id` | string | Yes | Pricing option from product. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) | +| `format_ids` | FormatID[] | Yes | Creative formats to use (must be supported by product) | +| `budget` | number | Yes | Budget in pricing option's currency | +| `pacing` | string | No | `"even"` (default), `"asap"`, or `"front_loaded"` | +| `bid_price` | number | No | Bid price for auction products | +| `targeting_overlay` | TargetingOverlay | No | Additional targeting (geographic restrictions, frequency caps). See [Targeting](/docs/media-buy/advanced-topics/targeting) | +| `creative_ids` | string[] | No | Existing creative IDs to assign | +| `creatives` | CreativeAsset[] | No | Upload and assign new creatives (max 100 per package) | + +## Response + +Returns a media buy with status indicating next steps: + +| Field | Description | +|-------|-------------| +| `media_buy_id` | Unique identifier for this media buy | +| `status` | `"completed"`, `"working"`, or `"submitted"` | +| `packages` | Array of created packages with IDs and statuses | +| `message` | Human-readable status description | + +**Status meanings:** +- **`completed`**: Media buy is active and ready +- **`working`**: Processing asynchronously (< 120 seconds expected) +- **`submitted`**: Requires manual approval (hours to days) + +See [schema](https://adcontextprotocol.org/schemas/v1/media-buy/create-media-buy-response.json) for complete field list. + +## Common Scenarios + +### Multi-Package Campaign + + + +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; + +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } -} -``` - -**Webhook Notifications (sent to buyer's endpoint):** -```json -// First webhook -{ - "taskId": "task-mb-webhook-001", - "contextId": "ctx-conversation-xyz", - "status": {"state": "working"}, - "message": "Budget exceeds threshold. Awaiting sales approval." -} - -// Final webhook when complete -{ - "taskId": "task-mb-webhook-001", - "contextId": "ctx-conversation-xyz", - "status": {"state": "completed"}, - "artifacts": [{ - "artifactId": "artifact-mb-webhook-xyz", - "name": "media_buy_confirmation", - "parts": [ - {"kind": "data", "data": { - "media_buy_id": "mb_789456", - "buyer_ref": "large_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [...] - }} - ] - }] -} -``` - -### A2A with Input Required - -If the system needs clarification (e.g., ambiguous targeting): - -**SSE Update requesting input:** -``` -data: {"status": {"state": "input-required", "message": "Multiple interpretations found for 'sports fans'. Please specify: 1) All sports enthusiasts, 2) NFL fans specifically, 3) Live sports event viewers"}} -``` +}]); -**Client provides clarification:** -```json -{ - "referenceTaskIds": ["task-mb-large-001"], - "message": "Target all sports enthusiasts including NFL, NBA, and soccer fans" -} -``` +const agent = client.agent('test-agent'); -**Task resumes with clarification:** -``` -data: {"status": {"state": "working", "message": "Applying targeting for all sports enthusiasts..."}} -data: {"status": {"state": "completed"}, "artifacts": [...]} -``` - -## Scenarios - -### Media Buy with Inline Creatives (Single Atomic Operation) +// Discover multiple products +const discovery = await agent.getProducts({ + filters: { + format_types: ['video', 'audio'], + delivery_type: 'guaranteed' + } +}); -Create a media buy and upload creatives in a single API call. This eliminates the need for a separate `sync_creatives` call and ensures creatives and campaign are created together atomically. +// Find video and audio products +const videoProduct = discovery.products.find(p => + p.format_ids.some(f => f.id.includes('video')) +); +const audioProduct = discovery.products.find(p => + p.format_ids.some(f => f.id.includes('audio')) +); -```json -{ - "buyer_ref": "nike_q1_campaign_2024", - "brand_manifest": { - "brand": { - "name": "Nike", - "website": "https://nike.com" - }, - "products": [ - { - "sku": "AIR_MAX_2024", - "name": "Air Max 2024", - "category": "athletic footwear", - "price": {"amount": 150, "currency": "USD"} - } - ] +// Create campaign with multiple packages +const mediaBuy = await agent.createMediaBuy({ + buyer_ref: `multi-package-${Date.now()}`, + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' }, - "promoted_products": { - "skus": ["AIR_MAX_2024"] - }, - "packages": [ + packages: [ { - "buyer_ref": "nike_ctv_package", - "product_id": "ctv_sports_premium", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_30s" - } - ], - "budget": 50000, - "pricing_option_id": "cpm-fixed-sports", - "creatives": [ - { - "creative_id": "hero_video_30s", - "name": "Air Max Hero Video", - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard_30s" - }, - "assets": { - "video": { - "asset_type": "video", - "url": "https://cdn.nike.com/hero-30s.mp4", - "width": 1920, - "height": 1080, - "duration_ms": 30000 - } - }, - "tags": ["q1_2024", "hero"] - } - ] + buyer_ref: 'video-package', + product_id: videoProduct.product_id, + pricing_option_id: videoProduct.pricing_options[0].pricing_option_id, + format_ids: [videoProduct.format_ids[0]], + budget: 30000 }, { - "buyer_ref": "nike_display_package", - "product_id": "display_premium", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "premium_bespoke_display" - } - ], - "budget": 30000, - "pricing_option_id": "cpm-fixed-display", - "creatives": [ - { - "creative_id": "holiday_hero_display", - "name": "Air Max Holiday Display (Generative)", - "format_id": { - "agent_url": "https://publisher.com/.well-known/adcp/sales", - "id": "premium_bespoke_display" - }, - "assets": { - "promoted_offerings": { - "asset_type": "promoted_offerings", - "url": "https://nike.com/air-max", - "colors": { - "primary": "#111111", - "secondary": "#FFFFFF" - } - }, - "generation_prompt": { - "asset_type": "text", - "content": "Create a bold, athletic display ad emphasizing innovation and performance. Target runners aged 25-45." - } - }, - "tags": ["q1_2024", "generative"] - } - ] + buyer_ref: 'audio-package', + product_id: audioProduct.product_id, + pricing_option_id: audioProduct.pricing_options[0].pricing_option_id, + format_ids: [audioProduct.format_ids[0]], + budget: 20000 } ], - "start_time": "asap", - "end_time": "2024-03-31T23:59:59Z" -} -``` + start_time: 'asap', + end_time: '2024-12-31T23:59:59Z' +}); -**Benefits:** -- **Single API call** - Creates media buy and uploads both static and generative creatives atomically -- **Simplified workflow** - No need to manage creative library separately for new campaigns -- **Atomic operation** - If media buy fails, creatives aren't orphaned in library -- **Mixed creative types** - Combine static assets (video) with generative formats (display) in same request -- **Brand context** - Generative creatives leverage brand_manifest for creation +console.log(`Created ${mediaBuy.packages.length}-package media buy`); +``` -**Note:** Creatives are still added to the library for reuse. Use `creative_ids` to reference existing library creatives, or `creatives` to upload new ones. +```python Python +from adcp import ADCPMultiAgentClient -### Standard Media Buy Request -```json -{ - "buyer_ref": "purina_pet_campaign_q1", - "packages": [ - { - "buyer_ref": "purina_ctv_package", - "product_id": "ctv_prime_time", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_standard_30s" - } - ], - "budget": 30000, - "pacing": "even", - "pricing_option_id": "cpm-fixed-ctv", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "NY"], - "axe_include_segment": "x7h4n", - "signals": ["auto_intenders_q1_2025"], - "frequency_cap": { - "suppress_minutes": 30 - } - } - }, - { - "buyer_ref": "purina_audio_package", - "product_id": "audio_drive_time", - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "audio_standard_30s" - } - ], - "budget": 20000, - "pricing_option_id": "cpm-fixed-audio", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "NY"] - } +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - ], - "promoted_offering": "Purina Pro Plan dog food - premium nutrition tailored for dogs' specific needs, promoting the new salmon and rice formula for sensitive skin and stomachs", - "po_number": "PO-2024-Q1-0123", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-02-29T23:59:59Z" -} -``` +}]) -### Retail Media Buy Request -```json -{ - "buyer_ref": "purina_albertsons_retail_q1", - "packages": [ - { - "buyer_ref": "purina_albertsons_conquest", - "product_id": "albertsons_competitive_conquest", - "format_ids": [ +agent = client.agent('test-agent') + +# Discover multiple products +discovery = agent.get_products( + filters={ + 'format_types': ['video', 'audio'], + 'delivery_type': 'guaranteed' + } +) + +# Find video and audio products +video_product = next((p for p in discovery['products'] + if any('video' in f['id'] for f in p['format_ids'])), None) +audio_product = next((p for p in discovery['products'] + if any('audio' in f['id'] for f in p['format_ids'])), None) + +# Create campaign with multiple packages +import time +media_buy = agent.create_media_buy( + buyer_ref=f"multi-package-{int(time.time() * 1000)}", + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + packages=[ { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_300x250" + 'buyer_ref': 'video-package', + 'product_id': video_product['product_id'], + 'pricing_option_id': video_product['pricing_options'][0]['pricing_option_id'], + 'format_ids': [video_product['format_ids'][0]], + 'budget': 30000 }, { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_728x90" + 'buyer_ref': 'audio-package', + 'product_id': audio_product['product_id'], + 'pricing_option_id': audio_product['pricing_options'][0]['pricing_option_id'], + 'format_ids': [audio_product['format_ids'][0]], + 'budget': 20000 } - ], - "budget": 75000, - "pacing": "even", - "pricing_option_id": "cpm-fixed-retail", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "AZ", "NV"], - "axe_include_segment": "x3f9q", - "axe_exclude_segment": "x2v8r", - "frequency_cap": { - "suppress_minutes": 60 - } - } - } - ], - "promoted_offering": "Purina Pro Plan dog food - premium nutrition tailored for dogs' specific needs", - "po_number": "PO-2024-RETAIL-0456", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-03-31T23:59:59Z" -} + ], + start_time='asap', + end_time='2024-12-31T23:59:59Z' +) + +print(f"Created {len(media_buy['packages'])}-package media buy") ``` -### Response - Success -**Message**: "Successfully created your $50,000 media buy targeting pet owners in CA and NY. The campaign will reach 2.5M users through Connected TV and Audio channels. Please upload creative assets by January 30 to activate the campaign. Campaign scheduled to run Feb 1-29." + -**Payload**: -```json -{ - "media_buy_id": "gam_1234567890", - "buyer_ref": "purina_pet_campaign_q1", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - { - "package_id": "gam_pkg_001", - "buyer_ref": "purina_ctv_package" - }, - { - "package_id": "gam_pkg_002", - "buyer_ref": "purina_audio_package" - } - ] -} -``` +### Geographic Targeting -### Response - Retail Media Success -**Message**: "Successfully created your $75,000 retail media campaign targeting competitive dog food buyers. The campaign will reach 450K Albertsons shoppers with deterministic purchase data. Creative assets must include co-branding and drive to Albertsons.com. Upload by January 30 to activate. Campaign runs Feb 1 - Mar 31." + -**Payload**: -```json -{ - "media_buy_id": "albertsons_mb_789012", - "buyer_ref": "purina_albertsons_retail_q1", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - { - "package_id": "albertsons_pkg_001", - "buyer_ref": "purina_albertsons_conquest" - } - ] -} -``` +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; + +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); -### Response - Pending Manual Approval -**Message**: "Your $50,000 media buy has been submitted for approval. Due to the campaign size, it requires manual review by our sales team. Expected approval time is 2-4 hours during business hours. You'll receive a notification once approved. Campaign scheduled for Feb 1 - Mar 31." +const agent = client.agent('test-agent'); -**Payload**: -```json -{ - "media_buy_id": "mb_789", - "buyer_ref": "nike_q1_campaign_2024", - "creative_deadline": null, - "packages": [] -} -``` +const discovery = await agent.getProducts({ + brief: 'Video campaign for US launch', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + } +}); -## Platform Behavior +const product = discovery.products[0]; -Different advertising platforms handle media buy creation differently: +// Campaign with geographic restrictions +const mediaBuy = await agent.createMediaBuy({ + buyer_ref: `geo-targeted-${Date.now()}`, + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, + packages: [{ + buyer_ref: 'us-package', + product_id: product.product_id, + pricing_option_id: product.pricing_options[0].pricing_option_id, + format_ids: product.format_ids, + budget: 50000, + targeting_overlay: { + geo_country_any_of: ['US'], + geo_region_any_of: ['CA', 'NY', 'TX'] + } + }], + start_time: 'asap', + end_time: '2024-12-31T23:59:59Z' +}); -- **Google Ad Manager (GAM)**: Creates Order with LineItems, requires approval -- **Kevel**: Creates Campaign with Flights, instant activation -- **Triton**: Creates Campaign for audio delivery +console.log(`Created geo-targeted media buy: ${mediaBuy.media_buy_id}`); +``` -## Status Values +```python Python +from adcp import ADCPMultiAgentClient +import time -Both protocols use standard task states: +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]) -- `working`: Task is in progress (includes waiting for approvals, processing, etc.) -- `input-required`: Needs clarification or additional information from client -- `completed`: Task finished successfully -- `failed`: Task encountered an error -- `cancelled`: Task was cancelled -- `rejected`: Task was rejected (e.g., policy violation) +agent = client.agent('test-agent') -**Note**: Specific business states (like "awaiting manual approval", "pending creative assets", etc.) are conveyed through the message field, not custom status values. This ensures consistency across protocols. +discovery = agent.get_products( + brief='Video campaign for US launch', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } +) -## Asynchronous Behavior +product = discovery['products'][0] -This operation can be either synchronous or asynchronous depending on the publisher's implementation and the complexity of the request. +# Campaign with geographic restrictions +media_buy = agent.create_media_buy( + buyer_ref=f"geo-targeted-{int(time.time() * 1000)}", + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + packages=[{ + 'buyer_ref': 'us-package', + 'product_id': product['product_id'], + 'pricing_option_id': product['pricing_options'][0]['pricing_option_id'], + 'format_ids': product['format_ids'], + 'budget': 50000, + 'targeting_overlay': { + 'geo_country_any_of': ['US'], + 'geo_region_any_of': ['CA', 'NY', 'TX'] + } + }], + start_time='asap', + end_time='2024-12-31T23:59:59Z' +) -### Synchronous Response -When the operation can be completed immediately (rare), the response includes the created media buy details directly. +print(f"Created geo-targeted media buy: {media_buy['media_buy_id']}") +``` -### Asynchronous Response -When the operation requires processing time, the response returns immediately with: -- A tracking identifier (`context_id` for MCP, `taskId` for A2A) -- Initial status (`"working"` for both MCP and A2A) -- Updates can be received via: - - **Polling**: Call status endpoints periodically (MCP and A2A) - - **Webhooks**: Register callback URLs for push notifications (MCP and A2A) - - **Streaming**: Use SSE or WebSockets for real-time updates (MCP and A2A) + -## Status Checking +### Campaign with Inline Creatives -### MCP Status Checking + -#### Option 1: Polling (create_media_buy_status) +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -For MCP implementations using polling, use this endpoint to check the status of an asynchronous media buy creation. +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); -#### Request -```json -{ - "context_id": "ctx-create-mb-456" // Required - from create_media_buy response -} -``` +const agent = client.agent('test-agent'); -#### Response Examples - -**Processing:** -```json -{ - "message": "Media buy creation in progress - validating inventory", - "context_id": "ctx-create-mb-456", - "status": "working", - "progress": { - "current_step": "inventory_validation", - "completed": 2, - "total": 5, - "unit_type": "steps", - "responsible_party": "system" +const discovery = await agent.getProducts({ + brief: 'Display campaign', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' } -} -``` +}); -**Completed:** -```json -{ - "message": "Successfully created your $50,000 media buy. Upload creative assets by Jan 30.", - "context_id": "ctx-create-mb-456", - "status": "completed", - "media_buy_id": "gam_1234567890", - "buyer_ref": "espn_sports_q1_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - {"package_id": "gam_pkg_001", "buyer_ref": "espn_ctv_sports"}, - {"package_id": "gam_pkg_002", "buyer_ref": "espn_audio_sports"} - ] -} -``` +const product = discovery.products[0]; -**Pending Manual Approval:** -```json -{ - "message": "Media buy requires manual approval. Sales team reviewing campaign.", - "context_id": "ctx-create-mb-456", - "status": "working", - "responsible_party": "publisher", - "action_detail": "Sales team reviewing campaign" -} +// Create media buy with creatives in single operation +const mediaBuy = await agent.createMediaBuy({ + buyer_ref: `inline-creative-${Date.now()}`, + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, + packages: [{ + buyer_ref: 'display-package', + product_id: product.product_id, + pricing_option_id: product.pricing_options[0].pricing_option_id, + format_ids: product.format_ids, + budget: 15000, + creatives: [{ + creative_id: 'summer-2024-display', + format_id: product.format_ids[0], + assets: [{ + asset_id: 'hero-image', + asset_type: 'image', + url: 'https://nike.com/creatives/summer-2024.jpg' + }] + }] + }], + start_time: 'asap', + end_time: '2024-12-31T23:59:59Z' +}); + +console.log(`Media buy created with inline creatives: ${mediaBuy.media_buy_id}`); ``` -#### Option 2: Webhooks (MCP) +```python Python +from adcp import ADCPMultiAgentClient +import time -Register a callback URL to receive push notifications for long-running operations. Webhooks are ONLY used when the initial response is `submitted`. +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]) -**Configuration:** -```javascript -const response = await session.call('create_media_buy', - { - buyer_ref: "campaign_2024", - packages: [...] - }, - { - webhook_url: "https://buyer.example.com/webhooks/adcp/create_media_buy/agent_id/op_id", - webhook_auth: { type: "bearer", credentials: "bearer-token-xyz" } - } -); -``` +agent = client.agent('test-agent') -**Response patterns:** -- **`completed`** - Synchronous success, webhook NOT called (you have the result) -- **`working`** - Will complete within ~120s, webhook NOT called (wait for response) -- **`submitted`** - Long-running operation, webhook WILL be called on status changes - -**Example webhook flow (only for `submitted` operations):** - -Webhook POST for human approval needed: -```http -POST /webhooks/adcp/create_media_buy/agent_id/op_id HTTP/1.1 -Host: buyer.example.com -Authorization: Bearer bearer-token-xyz -Content-Type: application/json - -{ - "operation_id": "op_id", - "task_id": "task_456", - "task_type": "create_media_buy", - "status": "input-required", - "message": "Campaign budget $150K requires approval to proceed", - "result": { - "buyer_ref": "campaign_2024" - } -} -``` +discovery = agent.get_products( + brief='Display campaign', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } +) -**Webhook POST when complete (after approval - full create_media_buy response):** -```http -POST /webhooks/adcp/create_media_buy/agent_id/op_id HTTP/1.1 -Host: buyer.example.com -Authorization: Bearer bearer-token-xyz -Content-Type: application/json - -{ - "operation_id": "op_id", - "task_id": "task_456", - "task_type": "create_media_buy", - "status": "completed", - "result": { - "media_buy_id": "mb_12345", - "buyer_ref": "campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "packages": [ - { - "package_id": "pkg_001", - "buyer_ref": "ctv_package" - } - ] - } -} -``` +product = discovery['products'][0] -Each webhook includes protocol fields plus a `result` object for the task-specific payload of that status. See **[Task Management: Webhook Integration](../../protocols/task-management.mdx#webhook-integration)** for complete details. +# Create media buy with creatives in single operation +media_buy = agent.create_media_buy( + buyer_ref=f"inline-creative-{int(time.time() * 1000)}", + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + packages=[{ + 'buyer_ref': 'display-package', + 'product_id': product['product_id'], + 'pricing_option_id': product['pricing_options'][0]['pricing_option_id'], + 'format_ids': product['format_ids'], + 'budget': 15000, + 'creatives': [{ + 'creative_id': 'summer-2024-display', + 'format_id': product['format_ids'][0], + 'assets': [{ + 'asset_id': 'hero-image', + 'asset_type': 'image', + 'url': 'https://nike.com/creatives/summer-2024.jpg' + }] + }] + }], + start_time='asap', + end_time='2024-12-31T23:59:59Z' +) -### A2A Status Checking +print(f"Media buy created with inline creatives: {media_buy['media_buy_id']}") +``` -A2A supports both SSE streaming and webhooks as shown in the examples above. Choose based on your needs: -- **SSE**: Best for real-time updates with persistent connection -- **Webhooks**: Best for long-running tasks or when client may disconnect + -### Polling Guidelines (when using polling): -- First 10 seconds: Every 1-2 seconds -- Next minute: Every 5-10 seconds -- After 1 minute: Every 30-60 seconds -- For manual approval (when message indicates approval needed): Every 5 minutes +## Async Workflows -### Handling Pending States -Orchestrators MUST handle pending states as normal operation flow: +### Handling `working` Status -1. Store the context_id for tracking -2. Monitor for updates via configured method (polling, webhooks, or streaming) -3. Handle eventual completion, rejection, or manual approval +When a media buy returns `status: "working"`, check status periodically: -### Example Pending Operation Flow +```javascript +// If status is "working", poll for completion +if (mediaBuy.status === 'working') { + // Wait a few seconds, then check status + await new Promise(resolve => setTimeout(resolve, 5000)); -```python -# 1. Create media buy -response = await mcp.call_tool("create_media_buy", { - "buyer_ref": "espn_sports_q1_2024", - "packages": [ - { - "buyer_ref": "espn_ctv_sports", - "product_id": "sports_ctv_premium", - "budget": 30000, - "pacing": "even", - "pricing_option_id": "cpm-fixed-sports", - "targeting_overlay": { - "geo_country_any_of": ["US"], - "geo_region_any_of": ["CA", "NY"], - "axe_include_segment": "x5j7w" - } - }, - { - "buyer_ref": "espn_audio_sports", - "product_id": "audio_sports_talk", - "budget": 20000, - "pricing_option_id": "cpm-fixed-audio", - "targeting_overlay": { - "geo_country_any_of": ["US"] - } - } - ], - "promoted_offering": "ESPN+ streaming service - exclusive UFC fights and soccer leagues, promoting annual subscription", - "po_number": "PO-2024-001", - "start_time": "2024-02-01T00:00:00Z", - "end_time": "2024-03-31T23:59:59Z" -}) - -# Check if async processing is needed -if response.get("status") == "working": - context_id = response["context_id"] - - # 2. Monitor for completion (polling example shown, but webhooks/streaming may be available) - while True: - status_response = await mcp.call_tool("create_media_buy_status", { - "context_id": context_id - }) - - if status_response["status"] == "completed": - # Operation completed successfully - media_buy_id = status_response["media_buy_id"] - break - elif status_response["status"] == "failed": - # Operation failed - handle_error(status_response["error"]) - break - elif status_response["status"] == "working" and "approval" in status_response.get("message", "").lower(): - # Requires human approval - may take hours/days - notify_user_of_pending_approval(status_response) - # Continue polling less frequently - await sleep(300) # Check every 5 minutes - else: - # Still processing - await sleep(10) # Poll every 10 seconds + // Check status (implementation depends on protocol) + // MCP: call get_media_buy or polling endpoint + // A2A: status updates via SSE +} ``` -## Platform Mapping +### Handling `submitted` Status -How media buy creation maps to different platforms: +When status is `"submitted"`, manual approval is required (hours to days): -- **Google Ad Manager**: Creates an Order with LineItems -- **Kevel**: Creates a Campaign with Flights -- **Triton Digital**: Creates a Campaign with Flights +```javascript +if (mediaBuy.status === 'submitted') { + console.log('Media buy submitted for manual review'); + console.log(`Message: ${mediaBuy.message}`); + // Set up webhook or periodic status checks +} +``` -## Format Workflow and Placeholder Creatives +See [Task Management](/docs/protocols/task-management) for protocol-specific async patterns. -### Why Format Specification is Required +## Error Handling -When creating a media buy, format specification serves critical purposes: +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `INVALID_REQUEST` | Missing required fields or invalid values | Check request parameters against schema | +| `PRODUCT_NOT_FOUND` | Product ID doesn't exist | Verify product_id from discovery | +| `FORMAT_NOT_SUPPORTED` | Format not supported by product | Check product.format_ids | +| `POLICY_VIOLATION` | Content policy restriction | Review policy message and adjust | +| `BUDGET_INSUFFICIENT` | Budget too low for product minimums | Increase budget or choose different product | -1. **Placeholder Creation**: Publisher creates placeholder creatives in ad server with correct format specifications -2. **Validation**: System validates that selected products actually support the requested formats -3. **Clear Expectations**: Both parties know exactly what creative formats are needed -4. **Progress Tracking**: Track which creative assets are missing vs. required -5. **Technical Setup**: Ad server configuration completed before actual creatives arrive +## Format Requirements -### Workflow Integration +Each package must specify `format_ids` to enable: +- **Placeholder creation** in ad servers +- **Clear asset expectations** for creative teams +- **Format validation** before trafficking -The complete media buy workflow with format awareness: +See [Creative Formats](/docs/creative/formats) for format specifications. -``` -1. list_creative_formats -> Get available format specifications -2. get_products -> Find products (returns format IDs they support) -3. Validate format compatibility -> Ensure products support desired formats -4. create_media_buy -> Specify formats for each package (REQUIRED) - └── Publisher creates placeholders in ad server - └── Both sides have clear creative requirements -5. sync_creatives -> Upload actual files matching the specified formats -6. Campaign activation -> Replace placeholders with real creatives -``` +## Policy Compliance -### Format Validation - -Publishers MUST validate that: -- All specified formats are supported by the product in each package -- Format specifications match those returned by `list_creative_formats` -- Creative requirements can be fulfilled within campaign timeline - -If validation fails, return an error: -```json -{ - "error": { - "code": "FORMAT_INCOMPATIBLE", - "message": "Product 'ctv_sports_premium' does not support format 'audio_standard_30s'", - "field": "packages[0].formats", - "supported_formats": ["video_standard_30s", "video_standard_15s"] - } -} -``` +Media buys undergo policy review for: +- Advertiser category restrictions +- Content guidelines +- Platform-specific policies -## Usage Notes - -- A media buy represents a complete advertising campaign with one or more packages -- Each package is based on a single product with specific targeting, budget allocation, and format requirements -- **Format specification is required** for each package - this enables placeholder creation and validation -- Both media buys and packages have `buyer_ref` fields for the buyer's reference tracking -- The `brand_manifest` field is required and provides brand identity, context, and product catalog (see [Brand Manifest](/docs/creative/brand-manifest) for guidance) -- Publishers will validate the promoted offering against their policies before creating the media buy -- Package-level targeting overlay applies additional criteria on top of product-level targeting -- The total budget is distributed across packages based on their individual `budget` settings (or proportionally if not specified) -- Budget supports multiple currencies via ISO 4217 currency codes -- AXE segments (`axe_include_segment` and `axe_exclude_segment`) enable advanced audience targeting within the targeting overlay -- Creative assets must be uploaded before the deadline for the campaign to activate -- Pending states are normal operational states, not errors -- Orchestrators MUST NOT treat pending states as errors - they are part of normal workflow +Policy violations may result in `submitted` status requiring manual approval or rejection. See [Policy Compliance](/docs/media-buy/media-buys/policy-compliance) for details. -## Policy Compliance +## Next Steps -The `promoted_offering` is validated during media buy creation. If a policy violation is detected, the API will return an error: +After creating a media buy: -```json -{ - "error": { - "code": "POLICY_VIOLATION", - "message": "Offering category not permitted on this publisher", - "field": "promoted_offering", - "suggestion": "Contact publisher for category approval process" - } -} -``` +1. **Upload Creatives**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) to provide creative assets +2. **Monitor Delivery**: Use [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery) to track performance +3. **Optimize**: Use [`provide_performance_feedback`](/docs/media-buy/task-reference/provide_performance_feedback) for optimization +4. **Update**: Use [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) to modify campaigns + +## Learn More -Publishers should ensure that: -- The promoted offering aligns with the selected packages -- Any uploaded creatives match the declared offering -- The campaign complies with all applicable advertising policies - -## Implementation Guide - -### Generating Helpful Messages - -The `message` field should provide a concise summary that includes: -- Total budget and key targeting parameters -- Expected reach or inventory details -- Clear next steps and deadlines -- Approval status and expected timelines - -```python -def generate_media_buy_message(media_buy, request): - if media_buy.status == "completed" and media_buy.creative_deadline: - return f"Successfully created your ${request.total_budget:,} media buy targeting {format_targeting(request.targeting_overlay)}. The campaign will reach {media_buy.estimated_reach:,} users. Please upload creative assets by {format_date(media_buy.creative_deadline)} to activate the campaign." - elif media_buy.status == "working" and media_buy.requires_approval: - return f"Your ${request.total_budget:,} media buy has been submitted for approval. {media_buy.approval_reason}. Expected approval time is {media_buy.estimated_approval_time}. You'll receive a notification once approved." - elif media_buy.status == "completed" and media_buy.is_live: - return f"Great news! Your ${request.total_budget:,} campaign is now live and delivering to your target audience. Monitor performance using check_media_buy_status." - elif media_buy.status == "rejected": - return f"Media buy was rejected: {media_buy.rejection_reason}. Please review the requirements and resubmit." -``` \ No newline at end of file +- [Media Buy Lifecycle](/docs/media-buy/media-buys/) - Complete campaign workflow +- [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) - CPM, CPCV, CPP explained +- [Targeting](/docs/media-buy/advanced-topics/targeting) - Targeting overlays and restrictions +- [Task Management](/docs/protocols/task-management) - Async patterns and status checking diff --git a/docs/media-buy/task-reference/get_media_buy_delivery.mdx b/docs/media-buy/task-reference/get_media_buy_delivery.mdx index 5dcf421a..6a3a6390 100644 --- a/docs/media-buy/task-reference/get_media_buy_delivery.mdx +++ b/docs/media-buy/task-reference/get_media_buy_delivery.mdx @@ -5,573 +5,366 @@ sidebar_position: 6 # get_media_buy_delivery -Retrieve comprehensive delivery metrics and performance data for reporting. +Retrieve comprehensive delivery metrics and performance data for media buy reporting. **Response Time**: ~60 seconds (reporting query) - -**Request Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-request.json) -**Response Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-response.json) +**Request Schema**: [`/schemas/v1/media-buy/get-media-buy-delivery-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-request.json) +**Response Schema**: [`/schemas/v1/media-buy/get-media-buy-delivery-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-response.json) ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `media_buy_ids` | string[] | No* | Array of publisher media buy IDs to get delivery data for | -| `buyer_refs` | string[] | No* | Array of buyer reference IDs to get delivery data for | -| `status_filter` | string \| string[] | No | Filter by status. Can be a single status or array of statuses: `"active"`, `"pending"`, `"paused"`, `"completed"`, `"failed"`, `"all"`. Defaults to `["active"]` | -| `start_date` | string | No | Start date for reporting period (YYYY-MM-DD) | -| `end_date` | string | No | End date for reporting period (YYYY-MM-DD) | - -*Either `media_buy_ids` or `buyer_refs` can be provided. If neither is provided, returns all media buys in the current session context. - -## Response (Message) - -The response includes a human-readable message that: -- Summarizes campaign performance and key insights -- Highlights pacing and completion rates -- Provides recommendations based on performance -- Explains any delivery issues or optimizations - -The message is returned differently in each protocol: -- **MCP**: Returned as a `message` field in the JSON response -- **A2A**: Returned as a text part in the artifact - -## Response (Payload) - -```json -{ - "reporting_period": { - "start": "string", - "end": "string" - }, - "currency": "string", - "aggregated_totals": { - "impressions": "number", - "spend": "number", - "clicks": "number", - "video_completions": "number", - "media_buy_count": "number" - }, - "media_buy_deliveries": [ - { - "media_buy_id": "string", - "buyer_ref": "string", - "status": "string", - "totals": { - "impressions": "number", - "spend": "number", - "clicks": "number", - "ctr": "number", - "video_completions": "number", - "completion_rate": "number" - }, - "by_package": [ - { - "package_id": "string", - "buyer_ref": "string", - "impressions": "number", - "spend": "number", - "clicks": "number", - "video_completions": "number", - "pacing_index": "number", - "pricing_model": "string", - "rate": "number", - "currency": "string" - } - ], - "daily_breakdown": [ - { - "date": "string", - "impressions": "number", - "spend": "number" - } - ] - } - ] -} -``` +| `media_buy_ids` | string[] | No* | Array of media buy IDs to retrieve | +| `buyer_refs` | string[] | No* | Array of buyer reference IDs | +| `status_filter` | string \| string[] | No | Status filter: `"active"`, `"pending"`, `"paused"`, `"completed"`, `"failed"`, `"all"`. Defaults to `["active"]` | +| `start_date` | string | No | Report start date (YYYY-MM-DD) | +| `end_date` | string | No | Report end date (YYYY-MM-DD) | + +*Either `media_buy_ids` or `buyer_refs` can be provided. If neither provided, returns all media buys in current session context. + +## Response + +Returns delivery report with aggregated totals and per-media-buy breakdowns: + +| Field | Description | +|-------|-------------| +| `reporting_period` | Date range for report (start/end timestamps) | +| `currency` | ISO 4217 currency code (USD, EUR, GBP, etc.) | +| `aggregated_totals` | Combined metrics across all media buys (impressions, spend, clicks, video_completions, media_buy_count) | +| `media_buy_deliveries` | Array of delivery data per media buy | + +### Media Buy Delivery Object + +| Field | Description | +|-------|-------------| +| `media_buy_id` | Media buy identifier | +| `buyer_ref` | Buyer's reference identifier | +| `status` | Current status (`pending`, `active`, `paused`, `completed`, `failed`) | +| `totals` | Aggregate metrics (impressions, spend, clicks, ctr, video_completions, completion_rate) | +| `by_package` | Package-level breakdowns with pacing_index | +| `daily_breakdown` | Day-by-day delivery (date, impressions, spend) | + +See [schema](https://adcontextprotocol.org/schemas/v1/media-buy/get-media-buy-delivery-response.json) for complete field list. + +## Common Scenarios -### Field Descriptions - -- **reporting_period**: Date range for the report - - **start**: ISO 8601 start timestamp - - **end**: ISO 8601 end timestamp -- **currency**: ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`) -- **aggregated_totals**: Combined metrics across all returned media buys - - **impressions**: Total impressions delivered across all media buys - - **spend**: Total amount spent across all media buys - - **clicks**: Total clicks across all media buys (if applicable) - - **video_completions**: Total video completions across all media buys (if applicable) - - **media_buy_count**: Number of media buys included in the response -- **media_buy_deliveries**: Array of delivery data for each media buy - - **media_buy_id**: Publisher's media buy identifier - - **buyer_ref**: Buyer's reference identifier for this media buy - - **status**: Current media buy status (`pending`, `active`, `paused`, `completed`, `failed`) - - **totals**: Aggregate metrics for this media buy across all packages - - **impressions**: Total impressions delivered - - **spend**: Total amount spent - - **clicks**: Total clicks (if applicable) - - **ctr**: Click-through rate (clicks/impressions) - - **video_completions**: Total video completions (if applicable) - - **completion_rate**: Video completion rate (completions/impressions) - - **by_package**: Metrics broken down by package - - **package_id**: Publisher's package identifier - - **buyer_ref**: Buyer's reference identifier for this package - - **impressions**: Package impressions - - **spend**: Package spend - - **clicks**: Package clicks - - **video_completions**: Package video completions - - **pacing_index**: Delivery pace (1.0 = on track, <1.0 = behind, >1.0 = ahead) - - **pricing_model**: The pricing model used for this package (e.g., `"cpm"`, `"cpcv"`, `"cpp"`, `"cpc"`, `"cpv"`, `"vcpm"`, `"flat_rate"`). This indicates how the package is billed and which metrics are most relevant for optimization. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) for details on each model. - - **rate**: The pricing rate for this package in the specified currency. For fixed-rate pricing, this is the agreed rate (e.g., CPM rate of `12.50` means $12.50 per 1,000 impressions). For auction-based pricing, this represents the effective rate based on actual delivery. The rate helps calculate expected delivery and compare performance across different pricing models. - - **currency**: ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`) for this specific package's pricing. This indicates the currency in which the `rate` and `spend` values are denominated. Different packages within the same media buy can use different currencies when supported by the publisher. - - **daily_breakdown**: Day-by-day delivery - - **date**: Date (YYYY-MM-DD) - - **impressions**: Daily impressions - - **spend**: Daily spend - -## Protocol-Specific Examples - -The AdCP payload is identical across protocols. Only the request/response wrapper differs. - -### MCP Request -```json -{ - "tool": "get_media_buy_delivery", - "arguments": { - "buyer_refs": ["nike_q1_campaign_2024"], - "start_date": "2024-01-01", - "end_date": "2024-01-31" - } -} +### Single Media Buy + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Get single media buy delivery report +const result = await testAgent.getMediaBuyDelivery({ + media_buy_ids: ['mb_12345'], + start_date: '2024-02-01', + end_date: '2024-02-07' +}); + +console.log(`Delivered ${result.aggregated_totals.impressions.toLocaleString()} impressions`); +console.log(`Spend: $${result.aggregated_totals.spend.toFixed(2)}`); +console.log(`CTR: ${(result.media_buy_deliveries[0].totals.ctr * 100).toFixed(2)}%`); ``` -### MCP Response -```json -{ - "message": "Campaign is 65% delivered with strong performance. CTR of 2.3% exceeds benchmark.", - "reporting_period": { - "start": "2024-01-01T00:00:00Z", - "end": "2024-01-31T23:59:59Z" - }, - "currency": "USD", - "aggregated_totals": { - "impressions": 1250000, - "spend": 32500, - "clicks": 28750, - "video_completions": 875000, - "media_buy_count": 1 - }, - "media_buy_deliveries": [ - { - "media_buy_id": "mb_12345", - "status": "active", - "totals": { - "impressions": 1250000, - "spend": 32500, - "clicks": 28750, - "ctr": 2.3, - "video_completions": 875000, - "completion_rate": 70 - }, - "by_package": [ - { - "package_id": "pkg_ctv_001", - "impressions": 750000, - "spend": 22500, - "clicks": 0, - "video_completions": 525000, - "pacing_index": 0.95, - "pricing_model": "cpcv", - "rate": 0.03, - "currency": "USD" - } - ] - } - ] -} +```python Python +from adcp.test_helpers import test_agent + +# Get single media buy delivery report +result = test_agent.get_media_buy_delivery( + media_buy_ids=['mb_12345'], + start_date='2024-02-01', + end_date='2024-02-07' +) + +print(f"Delivered {result['aggregated_totals']['impressions']:,} impressions") +print(f"Spend: ${result['aggregated_totals']['spend']:.2f}") +print(f"CTR: {result['media_buy_deliveries'][0]['totals']['ctr'] * 100:.2f}%") ``` -### A2A Request - -#### Natural Language Invocation -```javascript -await a2a.send({ - message: { - parts: [{ - kind: "text", - text: "Show me the delivery metrics for media buy mb_12345 from January 1st through January 31st, 2024." - }] - } + + +### Multiple Media Buys + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Get all active media buys from context +const result = await testAgent.getMediaBuyDelivery({ + status_filter: 'active', + start_date: '2024-02-01', + end_date: '2024-02-07' }); -``` -#### Explicit Skill Invocation -```javascript -await a2a.send({ - message: { - parts: [{ - kind: "data", - data: { - skill: "get_media_buy_delivery", - parameters: { - media_buy_ids: ["mb_12345"], - start_date: "2024-01-01", - end_date: "2024-01-31" - } - } - }] - } +console.log(`${result.aggregated_totals.media_buy_count} active campaigns`); +console.log(`Total impressions: ${result.aggregated_totals.impressions.toLocaleString()}`); +console.log(`Total spend: $${result.aggregated_totals.spend.toFixed(2)}`); + +// Review each campaign +result.media_buy_deliveries.forEach(delivery => { + console.log(`${delivery.media_buy_id}: ${delivery.totals.impressions.toLocaleString()} impressions, CTR ${(delivery.totals.ctr * 100).toFixed(2)}%`); }); ``` -### A2A Response -A2A returns results as artifacts: -```json -{ - "artifacts": [{ - "name": "delivery_report", - "parts": [ - { - "kind": "text", - "text": "Campaign is 65% delivered with strong performance. CTR of 2.3% exceeds benchmark." - }, - { - "kind": "data", - "data": { - "reporting_period": { - "start": "2024-01-01T00:00:00Z", - "end": "2024-01-31T23:59:59Z" - }, - "currency": "USD", - "aggregated_totals": { - "impressions": 1250000, - "spend": 32500, - "clicks": 28750, - "video_completions": 875000, - "media_buy_count": 1 - }, - "media_buy_deliveries": [ - { - "media_buy_id": "mb_12345", - "status": "active", - "totals": { - "impressions": 1250000, - "spend": 32500, - "clicks": 28750, - "ctr": 2.3, - "video_completions": 875000, - "completion_rate": 70 - }, - "by_package": [ - { - "package_id": "pkg_ctv_001", - "impressions": 750000, - "spend": 22500, - "clicks": 0, - "video_completions": 525000, - "pacing_index": 0.95, - "pricing_model": "cpcv", - "rate": 0.03, - "currency": "USD" - } - ] - } - ] - } - } - ] - }] -} +```python Python +from adcp.test_helpers import test_agent + +# Get all active media buys from context +result = test_agent.get_media_buy_delivery( + status_filter='active', + start_date='2024-02-01', + end_date='2024-02-07' +) + +print(f"{result['aggregated_totals']['media_buy_count']} active campaigns") +print(f"Total impressions: {result['aggregated_totals']['impressions']:,}") +print(f"Total spend: ${result['aggregated_totals']['spend']:.2f}") + +# Review each campaign +for delivery in result['media_buy_deliveries']: + print(f"{delivery['media_buy_id']}: {delivery['totals']['impressions']:,} impressions, CTR {delivery['totals']['ctr'] * 100:.2f}%") ``` -### Key Differences -- **MCP**: Direct tool call with arguments, returns flat JSON response -- **A2A**: Skill invocation with input, returns artifacts with text and data parts -- **Payload**: The `input` field in A2A contains the exact same structure as MCP's `arguments` + + +### Date Range Reporting + + -## Scenarios +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -### Example 1: Single Media Buy Query +// Get month-to-date performance +const now = new Date(); +const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); +const dateFormat = date => date.toISOString().split('T')[0]; -#### Request -```json -{ - "context_id": "ctx-media-buy-abc123", // From previous operations - "media_buy_ids": ["gam_1234567890"], - "start_date": "2024-02-01", - "end_date": "2024-02-07" -} +const result = await testAgent.getMediaBuyDelivery({ + media_buy_ids: ['mb_12345'], + start_date: dateFormat(monthStart), + end_date: dateFormat(now) +}); + +// Analyze daily breakdown +const dailyBreakdown = result.media_buy_deliveries[0].daily_breakdown; +console.log(`Daily average: ${Math.round(result.aggregated_totals.impressions / dailyBreakdown.length).toLocaleString()} impressions`); + +// Find peak day +const peakDay = dailyBreakdown.reduce((max, day) => + day.impressions > max.impressions ? day : max +); +console.log(`Peak day: ${peakDay.date} with ${peakDay.impressions.toLocaleString()} impressions`); ``` -#### Response - Strong Performance -**Message**: "Your campaign delivered 450,000 impressions this week with strong engagement. The 0.2% CTR exceeds industry benchmarks, and your video completion rate of 70% is excellent. You're currently pacing slightly behind (-9%) but should catch up with weekend delivery. Effective CPM is $37.50." - -**Payload**: -```json -{ - "reporting_period": { - "start": "2024-02-01T00:00:00Z", - "end": "2024-02-07T23:59:59Z" - }, - "currency": "USD", - "aggregated_totals": { - "impressions": 450000, - "spend": 16875.00, - "clicks": 900, - "video_completions": 315000, - "media_buy_count": 1 - }, - "media_buy_deliveries": [ - { - "media_buy_id": "gam_1234567890", - "status": "active", - "totals": { - "impressions": 450000, - "spend": 16875.00, - "clicks": 900, - "ctr": 0.002, - "video_completions": 315000, - "completion_rate": 0.70 - }, - "by_package": [ - { - "package_id": "pkg_ctv_prime_ca_ny", - "impressions": 250000, - "spend": 11250.00, - "clicks": 500, - "video_completions": 175000, - "pacing_index": 0.93, - "pricing_model": "cpcv", - "rate": 0.0643, - "currency": "USD" - }, - { - "package_id": "pkg_audio_drive_ca_ny", - "impressions": 200000, - "spend": 5625.00, - "clicks": 400, - "pacing_index": 0.88, - "pricing_model": "cpm", - "rate": 28.125, - "currency": "USD" - } - ], - "daily_breakdown": [ - { - "date": "2024-02-01", - "impressions": 64285, - "spend": 2410.71 - } - ] - } - ] -} +```python Python +from adcp.test_helpers import test_agent +from datetime import date + +# Get month-to-date performance +today = date.today() +month_start = date(today.year, today.month, 1) + +result = test_agent.get_media_buy_delivery( + media_buy_ids=['mb_12345'], + start_date=str(month_start), + end_date=str(today) +) + +# Analyze daily breakdown +daily_breakdown = result['media_buy_deliveries'][0]['daily_breakdown'] +daily_avg = result['aggregated_totals']['impressions'] // len(daily_breakdown) +print(f"Daily average: {daily_avg:,} impressions") + +# Find peak day +peak_day = max(daily_breakdown, key=lambda d: d['impressions']) +print(f"Peak day: {peak_day['date']} with {peak_day['impressions']:,} impressions") ``` -### Example 2: Multiple Media Buys with Status Filter + + +### Multi-Status Query + + -#### Request - Single Status -```json -{ - "context_id": "ctx-media-buy-abc123", - "status_filter": "active", // Only return active media buys - "start_date": "2024-02-01", - "end_date": "2024-02-07" -} +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Get both active and paused campaigns +const result = await testAgent.getMediaBuyDelivery({ + status_filter: ['active', 'paused'], + start_date: '2024-02-01', + end_date: '2024-02-07' +}); + +// Group by status +const byStatus = result.media_buy_deliveries.reduce((acc, delivery) => { + if (!acc[delivery.status]) acc[delivery.status] = []; + acc[delivery.status].push(delivery); + return acc; +}, {}); + +console.log(`Active campaigns: ${byStatus.active?.length || 0}`); +console.log(`Paused campaigns: ${byStatus.paused?.length || 0}`); + +// Identify underperforming campaigns +byStatus.paused?.forEach(delivery => { + const avgPacing = delivery.by_package.reduce((sum, pkg) => sum + pkg.pacing_index, 0) / delivery.by_package.length; + console.log(`${delivery.media_buy_id}: paused with ${(avgPacing * 100).toFixed(0)}% pacing`); +}); ``` -#### Request - Multiple Statuses -```json -{ - "context_id": "ctx-media-buy-abc123", - "status_filter": ["active", "paused"], // Return both active and paused media buys - "start_date": "2024-02-01", - "end_date": "2024-02-07" -} +```python Python +from adcp.test_helpers import test_agent +from collections import defaultdict + +# Get both active and paused campaigns +result = test_agent.get_media_buy_delivery( + status_filter=['active', 'paused'], + start_date='2024-02-01', + end_date='2024-02-07' +) + +# Group by status +by_status = defaultdict(list) +for delivery in result['media_buy_deliveries']: + by_status[delivery['status']].append(delivery) + +print(f"Active campaigns: {len(by_status['active'])}") +print(f"Paused campaigns: {len(by_status['paused'])}") + +# Identify underperforming campaigns +for delivery in by_status['paused']: + avg_pacing = sum(pkg['pacing_index'] for pkg in delivery['by_package']) / len(delivery['by_package']) + print(f"{delivery['media_buy_id']}: paused with {avg_pacing * 100:.0f}% pacing") ``` -#### Response - Multiple Active Campaigns -```json -{ - "message": "Your 3 active campaigns delivered 875,000 total impressions this week. Campaign performance varies: GAM campaign shows strong 0.2% CTR while Meta campaign needs attention with 0.08% CTR. Overall spend of $32,500 with average CPM of $37.14.", - "context_id": "ctx-media-buy-abc123", - "reporting_period": { - "start": "2024-02-01T00:00:00Z", - "end": "2024-02-07T23:59:59Z" - }, - "currency": "USD", - "aggregated_totals": { - "impressions": 875000, - "spend": 32500.00, - "clicks": 1400, - "video_completions": 481250, - "media_buy_count": 3 - }, - "media_buy_deliveries": [ - { - "media_buy_id": "gam_1234567890", - "status": "active", - "totals": { - "impressions": 450000, - "spend": 16875.00, - "clicks": 900, - "ctr": 0.002, - "video_completions": 315000, - "completion_rate": 0.70 - }, - "by_package": [ - { - "package_id": "pkg_ctv_prime_ca_ny", - "impressions": 250000, - "spend": 11250.00, - "clicks": 500, - "video_completions": 175000, - "pacing_index": 0.93, - "pricing_model": "cpcv", - "rate": 0.0643, - "currency": "USD" - } - ], - "daily_breakdown": [] - }, - { - "media_buy_id": "meta_9876543210", - "status": "active", - "totals": { - "impressions": 125000, - "spend": 5625.00, - "clicks": 100, - "ctr": 0.0008, - "video_completions": 56250, - "completion_rate": 0.45 - }, - "by_package": [ - { - "package_id": "pkg_social_feed", - "impressions": 125000, - "spend": 5625.00, - "clicks": 100, - "video_completions": 56250, - "pacing_index": 0.75, - "pricing_model": "cpm", - "rate": 45.00, - "currency": "USD" - } - ], - "daily_breakdown": [] - }, - { - "media_buy_id": "ttd_5555555555", - "status": "active", - "totals": { - "impressions": 300000, - "spend": 10000.00, - "clicks": 400, - "ctr": 0.00133, - "video_completions": 110000, - "completion_rate": 0.37 - }, - "by_package": [ - { - "package_id": "pkg_open_exchange", - "impressions": 300000, - "spend": 10000.00, - "clicks": 400, - "video_completions": 110000, - "pacing_index": 1.05, - "pricing_model": "cpm", - "rate": 33.33, - "currency": "USD" - } - ], - "daily_breakdown": [] - } - ] -} + + +### Buyer Reference Query + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Query by buyer reference instead of media buy ID +const result = await testAgent.getMediaBuyDelivery({ + buyer_refs: ['nike_q1_campaign_2024', 'nike_q1_retargeting_2024'] +}); + +// Lifetime delivery data (no date range specified) +console.log(`Total lifetime impressions: ${result.aggregated_totals.impressions.toLocaleString()}`); +console.log(`Total lifetime spend: $${result.aggregated_totals.spend.toFixed(2)}`); + +// Compare campaigns +result.media_buy_deliveries.forEach(delivery => { + const cpm = (delivery.totals.spend / delivery.totals.impressions) * 1000; + console.log(`${delivery.buyer_ref}: CPM $${cpm.toFixed(2)}, CTR ${(delivery.totals.ctr * 100).toFixed(2)}%`); +}); ``` -## Metrics Definitions +```python Python +from adcp.test_helpers import test_agent + +# Query by buyer reference instead of media buy ID +result = test_agent.get_media_buy_delivery( + buyer_refs=['nike_q1_campaign_2024', 'nike_q1_retargeting_2024'] +) -- **Impressions**: Number of times ads were displayed -- **Spend**: Amount spent in the specified currency -- **Clicks**: Number of times users clicked on ads -- **CTR (Click-Through Rate)**: Clicks divided by impressions -- **Video Completions**: Number of video ads watched to completion -- **Completion Rate**: Video completions divided by video impressions -- **Pacing Index**: Actual delivery rate vs. expected delivery rate - -## Usage Notes - -- If `media_buy_ids` is not provided, returns all media buys for the context -- Use the `status_filter` parameter to control which media buys are returned: - - Can be a single status string or an array of statuses - - Use `"all"` to return media buys of any status - - Defaults to `["active"]` if not specified -- If date range is not specified, returns lifetime delivery data -- Daily breakdown may be truncated for long campaigns or multiple media buys to reduce response size -- Some metrics (clicks, completions) may not be available for all formats -- Reporting data typically has a 2-4 hour delay -- Currency is always specified to avoid ambiguity - -### Aggregated Fields for Multi-Buy Queries - -When querying multiple media buys, the response includes `aggregated_totals` with: -- **impressions**: Sum of all impressions across returned media buys -- **spend**: Total spend across all returned media buys -- **clicks**: Total clicks (where available) -- **video_completions**: Total video completions (where available) -- **media_buy_count**: Number of media buys included in the response - -These aggregated fields provide a quick overview of overall campaign performance, while the `deliveries` array contains detailed metrics for each individual media buy. - -## Implementation Guide - -### Generating Performance Messages - -The `message` field should provide actionable insights: - -```python -def generate_delivery_message(report): - # Calculate key performance indicators - cpm = (report.totals.spend / report.totals.impressions) * 1000 - avg_pacing = calculate_average_pacing(report.by_package) - - # Analyze performance - performance_level = analyze_performance(report.totals.ctr, report.totals.completion_rate) - pacing_status = "on track" if avg_pacing > 0.95 else f"{int((1-avg_pacing)*100)}% behind" - - # Generate insights - insights = [] - if performance_level == "strong": - insights.append(f"The {report.totals.ctr:.1%} CTR exceeds industry benchmarks") - if report.totals.completion_rate: - insights.append(f"your video completion rate of {report.totals.completion_rate:.0%} is excellent") - else: - insights.append(f"the {report.totals.ctr:.2%} CTR is below expectations") - if report.totals.completion_rate < 0.5: - insights.append("completion rate suggests creative fatigue") - - # Build message - return f"Your campaign delivered {report.totals.impressions:,} impressions {get_time_period(report.reporting_period)} with {performance_level} engagement. {'. '.join(insights)}. You're currently pacing {pacing_status}. Effective CPM is ${cpm:.2f}." +# Lifetime delivery data (no date range specified) +print(f"Total lifetime impressions: {result['aggregated_totals']['impressions']:,}") +print(f"Total lifetime spend: ${result['aggregated_totals']['spend']:.2f}") + +# Compare campaigns +for delivery in result['media_buy_deliveries']: + cpm = (delivery['totals']['spend'] / delivery['totals']['impressions']) * 1000 + print(f"{delivery['buyer_ref']}: CPM ${cpm:.2f}, CTR {delivery['totals']['ctr'] * 100:.2f}%") ``` -## Platform-Specific Metrics + + +## Metrics Definitions + +| Metric | Definition | +|--------|------------| +| **Impressions** | Number of times ads were displayed | +| **Spend** | Amount spent in specified currency | +| **Clicks** | Number of ad clicks (if available) | +| **CTR** | Click-through rate (clicks/impressions) | +| **Video Completions** | Videos watched to completion | +| **Completion Rate** | Video completions/video impressions | +| **Pacing Index** | Actual vs. expected delivery rate (1.0 = on track, <1.0 = behind, >1.0 = ahead) | +| **CPM** | Cost per thousand impressions (spend/impressions * 1000) | + +## Query Behavior + +### Context-Based Queries +- If neither `media_buy_ids` nor `buyer_refs` provided, returns all media buys from current session context +- Context established by previous operations (e.g., `create_media_buy`) + +### Status Filtering +- Defaults to `["active"]` if not specified +- Can be single string (`"active"`) or array (`["active", "paused"]`) +- Use `"all"` to return media buys of any status + +### Date Ranges +- If dates not specified, returns lifetime delivery data +- Date format: `YYYY-MM-DD` +- Daily breakdown may be truncated for long date ranges to reduce response size + +### Metric Availability +- **Universal**: Impressions, spend (available on all platforms) +- **Format-dependent**: Clicks, video completions (depends on inventory type and platform capabilities) +- **Package-level**: All metrics broken down by package with pacing_index + +## Data Freshness + +- Reporting data typically has 2-4 hour delay +- Real-time impression counts not available +- Use for periodic reporting and optimization decisions, not live monitoring + +## Error Handling + +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `MEDIA_BUY_NOT_FOUND` | Media buy doesn't exist | Verify media_buy_id | +| `INVALID_DATE_RANGE` | Invalid start/end dates | Use YYYY-MM-DD format, ensure start < end | +| `CONTEXT_REQUIRED` | No media buys in context | Provide media_buy_ids or buyer_refs explicitly | +| `INVALID_STATUS_FILTER` | Invalid status value | Use valid status: active, pending, paused, completed, failed, all | + +## Best Practices + +**1. Use Date Ranges for Analysis** +Specify date ranges for period-over-period comparisons and trend analysis. + +**2. Monitor Pacing Index** +Aim for 0.95-1.05 pacing index. Values outside this range indicate delivery issues. + +**3. Check Daily Breakdown** +Identify delivery patterns and weekend/weekday performance differences. + +**4. Compare Package Performance** +Use `by_package` breakdowns to identify best-performing inventory. + +**5. Track Status Changes** +Use multi-status queries to understand why campaigns were paused or completed. + +## Next Steps -Different platforms return different metrics based on their capabilities: +After retrieving delivery data: -- **Total impressions delivered**: Available on all platforms -- **Total spend**: Available on all platforms -- **Clicks**: Available where click tracking is supported (display, video) -- **Video completions**: Available for video inventory on supporting platforms -- **Package-level breakdown**: Granular performance by package/flight +1. **Optimize Campaigns**: Use [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) to adjust budgets, pacing, or targeting +2. **Provide Feedback**: Use [`provide_performance_feedback`](/docs/media-buy/task-reference/provide_performance_feedback) to share results with seller +3. **Update Creatives**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) to refresh underperforming assets +4. **Create Follow-Up Campaigns**: Use [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) based on insights -## Usage Notes +## Learn More -- If dates are not specified, returns lifetime delivery data -- Metrics availability depends on the media type and platform capabilities -- Video metrics (completions, completion rate) only appear for video campaigns -- The pacing index helps identify delivery issues (aim for 0.95-1.05) -- Daily breakdown may be truncated for long date ranges -- Use this data for regular reporting and optimization decisions \ No newline at end of file +- [Media Buy Lifecycle](/docs/media-buy/media-buys/) - Complete campaign workflow +- [Task Management](/docs/protocols/task-management) - Async patterns and status handling +- [Performance Optimization](/docs/media-buy/media-buys/optimization-reporting) - Using delivery data for optimization diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx index 0e608c07..68683cda 100644 --- a/docs/media-buy/task-reference/get_products.mdx +++ b/docs/media-buy/task-reference/get_products.mdx @@ -1,999 +1,530 @@ --- title: get_products sidebar_position: 1 +testable: true --- # get_products -Discover available advertising products based on campaign requirements, using natural language briefs or structured filters. -**Authentication**: Optional (returns limited results without credentials - see [Authentication](/docs/reference/authentication.mdx#when-authentication-is-required)) +Discover available advertising products based on campaign requirements using natural language briefs or structured filters. -**Response Time**: ~60 seconds (inference/RAG with back-end systems) +**Authentication**: Optional (returns limited results without credentials) -**Pricing Information**: Products include pricing options that buyers select when creating media buys. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) for complete details on CPM, CPCV, CPP, and other pricing models. +**Response Time**: ~60 seconds (AI inference with back-end systems) -**Format Discovery**: Products return format references (IDs only). Use [`list_creative_formats`](/docs/media-buy/task-reference/list_creative_formats) to get full format specifications. **See [Creative Lifecycle](/docs/media-buy/creatives) for the complete workflow.** +**Request Schema**: [`/schemas/v1/media-buy/get-products-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-products-request.json) +**Response Schema**: [`/schemas/v1/media-buy/get-products-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-products-response.json) -**Request Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/get-products-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-products-request.json) -**Response Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/get-products-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-products-response.json) +## Quick Start -## Authentication Behavior - -- **Without credentials**: Returns limited catalog (run-of-network products), no pricing information, no custom offerings -- **With credentials**: Returns complete catalog, pricing details (CPM), custom products, and full targeting options - -See the [Quickstart Guide](/docs/quickstart.mdx#understanding-authentication) for details on getting credentials. +Discover products with a natural language brief: -## Request Parameters + -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `brief` | string | No | Natural language description of campaign requirements | -| `brand_manifest` | BrandManifest \| string | No | Brand information manifest providing brand context, assets, and product catalog. Can be provided inline as an object or as a URL reference to a hosted manifest. Sales agents can decide whether brand context is necessary for product recommendations. | -| `filters` | Filters | No | Structured filters for product discovery (see Filters Object below) | +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -### Filters Object +const result = await testAgent.getProducts({ + brief: 'Premium athletic footwear with innovative cushioning', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + } +}); -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `delivery_type` | string | No | Filter by delivery type: `"guaranteed"` or `"non_guaranteed"` | -| `is_fixed_price` | boolean | No | Filter for fixed price vs auction products | -| `format_types` | string[] | No | Filter by format types (e.g., `["video", "display"]`) | -| `format_ids` | FormatID[] | No | Filter by specific structured format ID objects | -| `standard_formats_only` | boolean | No | Only return products accepting IAB standard formats | -| `min_exposures` | integer | No | Minimum exposures/impressions needed for measurement validity | -## Response (Message) -The response includes a human-readable message that: -- Summarizes products found (e.g., "Found 3 premium video products matching your requirements") -- Explains why products match the brief -- Requests clarification if needed -- Explains policy restrictions if applicable - -The message is returned differently in each protocol: -- **MCP**: Returned as a `message` field in the JSON response -- **A2A**: Returned as a text part in the artifact - -## Response (Payload) - -Products include **EITHER** `properties` (for specific property lists) **OR** `property_tags` (for large networks), but never both. - -### Option A: Direct Properties -```json -{ - "products": [ - { - "product_id": "string", - "name": "string", - "description": "string", - "properties": [ - { - "property_type": "website|mobile_app|ctv_app|dooh|podcast|radio|streaming_audio", - "name": "string", - "identifiers": [ - { - "type": "string", - "value": "string" - } - ], - "tags": ["string"], - "publisher_domain": "string" - } - ], - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_30s_hosted" - } - ], - "delivery_type": "string", - "is_fixed_price": "boolean", - "cpm": "number", - "min_spend": "number", - "measurement": { - "type": "string", - "attribution": "string", - "window": "string", - "reporting": "string" - }, - "creative_policy": { - "co_branding": "string", - "landing_page": "string", - "templates_available": "boolean" - }, - "is_custom": "boolean", - "brief_relevance": "string" - } - ] +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} products`); } ``` -### Option B: Property Tags (for Large Networks) -```json -{ - "products": [ - { - "product_id": "local_radio_midwest", - "name": "Midwest Radio Network", - "description": "500+ local radio stations across midwest markets", - "property_tags": ["local_radio", "midwest"], - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "audio_30s" - }, - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "audio_60s" +```python Python +import asyncio +from adcp import test_agent + +async def discover_products(): + result = await test_agent.simple.get_products( + brief='Premium athletic footwear with innovative cushioning', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 25.00, - "currency": "USD", - "min_spend": 5000 - } - ] -} + ) + print(f"Found {len(result.products)} products") + +asyncio.run(discover_products()) ``` -### Field Descriptions -- **product_id**: Unique identifier for the product -- **name**: Human-readable product name -- **description**: Detailed description of the product and its inventory -- **pricing_options**: Array of available pricing models for this product. Each option has a unique `pricing_option_id` that buyers reference in `create_media_buy`. See [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) for complete documentation of supported pricing models (CPM, CPCV, CPP, CPC, vCPM, flat_rate). -- **properties**: Array of specific advertising properties covered by this product (see [Property Schema](https://adcontextprotocol.org/schemas/v1/core/property.json)) - - **property_type**: Type of advertising property ("website", "mobile_app", "ctv_app", "dooh", "podcast", "radio", "streaming_audio") - - **name**: Human-readable property name - - **identifiers**: Array of identifiers for this property - - **type**: Type of identifier (e.g., "domain", "bundle_id", "roku_store_id", "podcast_guid") - - **value**: The identifier value. For domain type: `"example.com"` matches www.example.com and m.example.com only; `"subdomain.example.com"` matches that specific subdomain; `"*.example.com"` matches all subdomains - - **tags**: Optional array of tags for categorization (e.g., network membership, content categories) - - **publisher_domain**: Domain where adagents.json should be checked for authorization validation -- **property_tags**: Array of tags referencing groups of properties (alternative to `properties` array) - - Use [`list_authorized_properties`](/docs/media-buy/task-reference/list_authorized_properties) to resolve tags to actual property objects - - Recommended for products with large property sets (e.g., radio networks with 1000+ stations) -- **format_ids**: Array of supported creative format ID objects (structured with `agent_url` and `id` fields) - use `list_creative_formats` to get full format details -- **delivery_type**: Either `"guaranteed"` or `"non_guaranteed"` -- **is_fixed_price**: Whether this product has fixed pricing (true) or uses auction (false) -- **cpm**: Cost per thousand impressions (for guaranteed/fixed price products) -- **currency**: ISO 4217 currency code (e.g., "USD", "EUR", "GBP") -- **min_spend**: Minimum budget requirement -- **estimated_exposures**: Estimated exposures/impressions for guaranteed products (optional) -- **floor_cpm**: Minimum CPM for non-guaranteed products - bids below this are rejected (optional) -- **recommended_cpm**: Recommended CPM to achieve min_exposures target for non-guaranteed products (optional) -- **measurement**: Included measurement capabilities (optional) - - **type**: Type of measurement (e.g., "incremental_sales_lift", "brand_lift", "foot_traffic") - - **attribution**: Attribution methodology (e.g., "deterministic_purchase", "probabilistic") - - **window**: Attribution window (e.g., "30_days", "7_days") - - **reporting**: Reporting frequency and format (e.g., "weekly_dashboard", "real_time_api") -- **reporting_capabilities**: Automated reporting capabilities (optional) - - **available_reporting_frequencies**: Supported frequencies for webhook-based reporting (e.g., ["hourly", "daily", "monthly"]) - - **expected_delay_minutes**: Expected delay in minutes before reporting data is available (e.g., 240 for 4 hours, 300 for 5 hours) - - **timezone**: Timezone for reporting periods - critical for daily/monthly alignment (e.g., "UTC", "America/New_York", "Europe/London") - - **supports_webhooks**: Whether webhook-based reporting notifications are available - - **available_metrics**: Metrics available in reporting - impressions and spend always implicitly included (e.g., ["impressions", "spend", "clicks", "video_completions", "conversions"]) -- **creative_policy**: Creative requirements and restrictions (optional) - - **co_branding**: Co-branding requirement ("required", "optional", "none") - - **landing_page**: Landing page requirements ("any", "retailer_site_only", "must_include_retailer") - - **templates_available**: Whether creative templates are provided -- **is_custom**: Whether this is a custom product -- **brief_relevance**: Explanation of why this product matches the brief (only included when brief is provided) - -## Property Tag Resolution - -When products use `property_tags` instead of full `properties` arrays, buyer agents must resolve the tags to actual property objects using [`list_authorized_properties`](/docs/media-buy/task-reference/list_authorized_properties). - -### Resolution Process - -1. **Call list_authorized_properties**: Get all properties from the sales agent (cache this response) -2. **Filter by tags**: Find properties where the `tags` array includes the referenced tags -3. **Use for validation**: Use the resolved properties for authorization validation - -### Example - -**Product with tags**: -```json -{ - "product_id": "local_radio_midwest", - "property_tags": ["local_radio", "midwest"] -} + +```bash CLI +uvx adcp \ + https://test-agent.adcontextprotocol.org/mcp \ + get_products \ + '{"brief":"Premium athletic footwear with innovative cushioning","brand_manifest":{"name":"Nike","url":"https://nike.com"}}' \ + --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ ``` -**Resolve via list_authorized_properties**: -```javascript -// 1. Get all authorized properties (cache this) -const authorized = await agent.list_authorized_properties(); + -// 2. Resolve tags to properties -const productProperties = authorized.properties.filter(prop => - prop.tags.includes("local_radio") && prop.tags.includes("midwest") -); +### Using Structured Filters -// 3. Use resolved properties for validation -for (const property of productProperties) { - await validateProperty(property); -} -``` +You can also use structured filters instead of (or in addition to) a brief: -**Why use tags?**: For large networks (e.g., 1847 radio stations), including all properties in every product response would create massive payloads. Tags provide efficient references while maintaining full validation capability. - -## Buyer Agent Validation - -**IMPORTANT**: Buyer agents MUST validate sales agent authorization before purchasing inventory to prevent unauthorized reselling. - -### Validation Requirements - -1. **Get Properties**: For each product, get property objects either: - - Directly from the `properties` array, OR - - By resolving `property_tags` via [`list_authorized_properties`](/docs/media-buy/task-reference/list_authorized_properties) -2. **Check Publisher Domains**: For each property, fetch `/.well-known/adagents.json` from `publisher_domain` -3. **Validate Domain Identifiers**: For website properties, also check each domain identifier -4. **Validate Agent**: Confirm the sales agent URL appears in `authorized_agents` -5. **Scope Matching**: Compare `authorized_for` description with product details -6. **Reject Unauthorized**: Decline products from unauthorized agents - -### Example Validation - -**Product Response**: -```json -{ - "product_id": "yahoo-premium-video", - "name": "Yahoo Premium Video Package", - "properties": [ - { - "property_type": "website", - "name": "Yahoo News & Finance Network", - "identifiers": [ - {"type": "domain", "value": "yahoo.com"}, - {"type": "domain", "value": "finance.yahoo.com"}, - {"type": "network_id", "value": "yahoo_network"} - ], - "tags": ["yahoo_network", "news_media", "premium_content"], - "publisher_domain": "yahoo.com" - } - ] -} -``` + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -### Domain Matching Examples +const result = await testAgent.getProducts({ + filters: { + format_types: ['video'], + delivery_type: 'guaranteed', + standard_formats_only: true + } +}); -#### Base Domain Matching -```json -{ - "identifiers": [ - {"type": "domain", "value": "newssite.com"} - ] -} -``` -**Matches**: `newssite.com`, `www.newssite.com`, `m.newssite.com` - -#### Specific Subdomain Matching -```json -{ - "identifiers": [ - {"type": "domain", "value": "sports.newssite.com"} - ] -} -``` -**Matches**: `sports.newssite.com` only - -#### Wildcard Subdomain Matching -```json -{ - "identifiers": [ - {"type": "domain", "value": "*.newssite.com"} - ] +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} guaranteed video products`); } ``` -**Matches**: All subdomains (`sports.newssite.com`, `finance.newssite.com`, etc.) but not the base domain - -#### Combined Authorization Strategy -```json -{ - "identifiers": [ - {"type": "domain", "value": "newsnetwork.com"}, - {"type": "domain", "value": "*.newsnetwork.com"} - ] -} + +```python Python +import asyncio +from adcp import test_agent + +async def discover_with_filters(): + result = await test_agent.simple.get_products( + filters={ + 'format_types': ['video'], + 'delivery_type': 'guaranteed', + 'standard_formats_only': True + } + ) + print(f"Found {len(result.products)} guaranteed video products") + +asyncio.run(discover_with_filters()) ``` -**Matches**: Base domain, www/m subdomains, and all other subdomains -**Required Checks**: -- Fetch `yahoo.com/.well-known/adagents.json` (from `publisher_domain`) -- Also validate domain identifiers: check `yahoo.com` and `finance.yahoo.com` -- Verify sales agent is authorized in adagents.json -- Validate scope matches product description + -For complete validation requirements, examples, and error handling, see the [adagents.json Tech Spec](/docs/media-buy/capability-discovery/adagents#buyer-agent-validation) documentation. +## Request Parameters -## Domain Matching Examples +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `brief` | string | No | Natural language description of campaign requirements | +| `brand_manifest` | BrandManifest \| string | No | Brand information (inline object or URL). See [Brand Manifest](/docs/creative/brand-manifest) | +| `filters` | Filters | No | Structured filters (see below) | -When validating authorization, domain matching follows specific rules for identifier values: +### Filters Object -### Base Domain Matching -```json -{ - "identifiers": [{"type": "domain", "value": "nytimes.com"}] -} -``` -**Matches**: `www.nytimes.com`, `m.nytimes.com` -**Does NOT Match**: `cooking.nytimes.com`, `games.nytimes.com` +| Parameter | Type | Description | +|-----------|------|-------------| +| `delivery_type` | string | Filter by `"guaranteed"` or `"non_guaranteed"` | +| `is_fixed_price` | boolean | Filter for fixed price vs auction products | +| `format_types` | string[] | Filter by format types (e.g., `["video", "display"]`) | +| `format_ids` | FormatID[] | Filter by specific format IDs | +| `standard_formats_only` | boolean | Only return products accepting IAB standard formats | +| `min_exposures` | integer | Minimum exposures needed for measurement validity | -### Specific Subdomain Matching -```json -{ - "identifiers": [{"type": "domain", "value": "cooking.nytimes.com"}] -} -``` -**Matches**: `cooking.nytimes.com` only -**Does NOT Match**: `www.nytimes.com`, `games.nytimes.com` +## Response -### Wildcard Subdomain Matching -```json -{ - "identifiers": [{"type": "domain", "value": "*.nytimes.com"}] -} -``` -**Matches**: `cooking.nytimes.com`, `games.nytimes.com`, `travel.nytimes.com` -**Does NOT Match**: `www.nytimes.com`, `nytimes.com`, `subdomain.cooking.nytimes.com` -## Protocol-Specific Examples -The AdCP payload is identical across protocols. Only the request/response wrapper differs. -### MCP Request -```json -{ - "tool": "get_products", - "arguments": { - "brief": "Premium video inventory for sports fans", - "filters": { - "format_types": ["video"], - "delivery_type": "guaranteed" - } +Returns an array of `products`, each containing: + +| Field | Type | Description | +|-------|------|-------------| +| `product_id` | string | Unique product identifier | +| `name` | string | Human-readable product name | +| `description` | string | Detailed product description | +| `format_ids` | FormatID[] | Supported creative format IDs | +| `delivery_type` | string | `"guaranteed"` or `"non_guaranteed"` | +| `pricing_options` | PricingOption[] | Available pricing models (CPM, CPCV, etc.) | +| `properties` | Property[] | Specific properties (for direct property lists) | +| `property_tags` | PropertyTag[] | Property tags (for large networks) | +| `brief_relevance` | string | Why this product matches the brief (when brief provided) | + +**See schema for complete field list**: [`get-products-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/get-products-response.json) + +## Common Scenarios + +### Run-of-Network Discovery + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// No brief = run-of-network products for maximum reach +const result = await testAgent.getProducts({ + filters: { + delivery_type: 'non_guaranteed' } +}); + +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} run-of-network products`); } ``` -### MCP Response -```json -{ - "message": "Found 3 premium video products matching your requirements", - "context_id": "ctx-media-buy-123", - "products": [ - { - "product_id": "ctv_sports_premium", - "name": "CTV Sports Premium", - "description": "Premium CTV inventory on sports content", - "properties": [ - { - "property_type": "website", - "name": "Sports Network", - "identifiers": [ - {"type": "domain", "value": "sportsnetwork.com"} - ], - "tags": ["sports_content", "premium_content"], - "publisher_domain": "sportsnetwork.com" - } - ], - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_16x9_30s" + +```python Python +import asyncio +from adcp import test_agent + +async def discover_run_of_network(): + # No brief = run-of-network products for maximum reach + result = await test_agent.simple.get_products( + filters={ + 'delivery_type': 'non_guaranteed' } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 45.00, - "currency": "USD", - "min_spend": 10000, - "is_custom": false, - "brief_relevance": "Premium CTV with sports content alignment" - } - ] -} -``` -### A2A Request -A2A supports both natural language and explicit skill invocation: - -#### Natural Language Invocation -```javascript -await a2a.send({ - message: { - parts: [{ - kind: "text", - text: "Find premium video inventory for sports fans. Looking for guaranteed delivery." - }] - } -}); + ) + print(f"Found {len(result.products)} run-of-network products") + +asyncio.run(discover_run_of_network()) ``` -#### Explicit Skill Invocation -```javascript -await a2a.send({ - message: { - parts: [ - { - kind: "text", - text: "Looking for sports inventory for campaign" // Optional context - }, - { - kind: "data", - data: { - skill: "get_products", // Must match skill name in Agent Card - parameters: { - brief: "Premium video inventory for sports fans", - filters: { - format_types: ["video"], - delivery_type: "guaranteed" - } - } - } - } - ] + + +### Multi-Format Discovery + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find products supporting both video and display +const result = await testAgent.getProducts({ + brief: 'Brand awareness campaign with video and display', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, + filters: { + format_types: ['video', 'display'] } }); -``` -### A2A Response -A2A returns results as artifacts with text and data parts: -```json -{ - "artifacts": [{ - "name": "product_discovery_result", - "parts": [ - { - "kind": "text", - "text": "Found 3 premium video products matching your requirements" - }, - { - "kind": "data", - "data": { - "products": [ - { - "product_id": "ctv_sports_premium", - "name": "CTV Sports Premium", - "description": "Premium CTV inventory on sports content", - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_16x9_30s" - } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 45.00, - "min_spend": 10000, - "is_custom": false, - "brief_relevance": "Premium CTV with sports content alignment" - } - ] - } - } - ] - }] + +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} products supporting video and display`); } ``` -### Key Differences -- **MCP**: Direct tool call with arguments, returns flat JSON response -- **A2A**: Message-based invocation (natural language or explicit skill with parameters), returns artifacts with text and data parts -- **Payload**: The `parameters` field in A2A explicit invocation contains the exact same structure as MCP's `arguments` -## Minimum Exposures for Measurement -When buyers specify `min_exposures` in the request, products are filtered to only include those that can deliver the required volume for measurement validity. +```python Python +import asyncio +from adcp import test_agent + +async def discover_multi_format(): + # Find products supporting both video and display + result = await test_agent.simple.get_products( + brief='Brand awareness campaign with video and display', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + filters={ + 'format_types': ['video', 'display'] + } + ) + print(f"Found {len(result.products)} products supporting video and display") + +asyncio.run(discover_multi_format()) +``` -### Guaranteed vs Non-Guaranteed Products + -**Guaranteed products** provide fixed pricing and predictable delivery: -- `cpm`: Fixed cost per thousand impressions -- `estimated_exposures`: Total exposures you can expect with your budget +### Budget-Based Filtering -**Non-guaranteed products** use auction-based pricing: -- `floor_cpm`: Minimum bid that will be accepted -- `recommended_cpm`: Suggested bid to win enough inventory to meet min_exposures target + -### Example: Requesting Minimum Exposures +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -**Request:** -```json -{ - "filters": { - "min_exposures": 10000 +// Find cost-effective products +const result = await testAgent.getProducts({ + brief: 'Cost-effective video inventory for $10k budget', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' } -} -``` +}); -**Response includes products that can deliver 10K+ exposures:** -```json -{ - "products": [ - { - "product_id": "guaranteed_sports", - "is_fixed_price": true, - "cpm": 45.00, - "currency": "USD", - "estimated_exposures": 222000 // Well above 10K minimum - }, - { - "product_id": "programmatic_video", - "is_fixed_price": false, - "currency": "USD", - "floor_cpm": 5.00, - "recommended_cpm": 12.00 // Bid this to achieve 10K exposures - } - ] -} -``` +if (result.success && result.data) { + // Filter products by budget + const budget = 10000; + const affordable = result.data.products.filter(p => { + const lowestCPM = Math.min(...p.pricing_options.map(opt => opt.cpm || Infinity)); + const estimatedCost = (lowestCPM / 1000) * (p.min_exposures || 10000); + return estimatedCost <= budget; + }); -## Scenarios -### Request with Natural Language Brief -```json -{ - "brief": "Looking for premium sports inventory" + console.log(`Found ${affordable.length} products within $${budget} budget`); } ``` -### Request for Run-of-Network (No Brief) -```json -{ - "filters": { - "delivery_type": "non_guaranteed", - "format_types": ["video", "display"], - "standard_formats_only": true - } -} + +```python Python +import asyncio +from adcp import test_agent + +async def discover_budget_friendly(): + # Find cost-effective products + result = await test_agent.simple.get_products( + brief='Cost-effective video inventory for $10k budget', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } + ) + + # Filter products by budget + budget = 10000 + affordable = [p for p in result.products + if min((opt.get('cpm', float('inf')) for opt in p['pricing_options'])) / 1000 + * p.get('min_exposures', 10000) <= budget] + + print(f"Found {len(affordable)} products within ${budget} budget") + +asyncio.run(discover_budget_friendly()) ``` -### Request with Structured Filters -```json -{ - "brief": "Fitness enthusiasts interested in home workouts", - "filters": { - "delivery_type": "guaranteed", - "format_types": ["video"], - "is_fixed_price": true, - "standard_formats_only": true + + + +### Property Tag Resolution + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Get products with property tags +const result = await testAgent.getProducts({ + brief: 'Sports content', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' } +}); + +if (result.success && result.data) { + // Products with property_tags need resolution via list_authorized_properties + const productsWithTags = result.data.products.filter(p => p.property_tags && p.property_tags.length > 0); + console.log(`${productsWithTags.length} products use property tags (large networks)`); } ``` -### Retail Media Request -```json -{ - "brief": "Target pet owners who shop at our stores using our first-party data" -} -``` -### Response - Run-of-Network (No Recommendations) -**Message**: "Found 5 run-of-network products for maximum reach. These are our broadest inventory pools optimized for scale." - -**Payload**: -```json -{ - "products": [ - { - "product_id": "open_exchange_video", - "name": "Open Exchange - Video", - "description": "Programmatic video inventory across all publishers", - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard" + +```python Python +import asyncio +from adcp import test_agent + +async def discover_property_tags(): + # Get products with property tags + result = await test_agent.simple.get_products( + brief='Sports content', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' } - ], - "delivery_type": "non_guaranteed", - "is_fixed_price": false, - "currency": "USD", - "min_spend": 1000, - "floor_cpm": 5.00, - "recommended_cpm": 12.00, - "is_custom": false - // Note: No brief_relevance field since no brief was provided - } - // ... more products - ] -} + ) + + # Products with property_tags need resolution via list_authorized_properties + products_with_tags = [p for p in result.products + if p.get('property_tags') and len(p['property_tags']) > 0] + print(f"{len(products_with_tags)} products use property tags (large networks)") + +asyncio.run(discover_property_tags()) ``` -### Response - Products Found with Brief -**Message**: "I found 3 premium sports-focused products that match your requirements. Connected TV Prime Time offers the best reach at $45 CPM with guaranteed delivery. All options support standard video formats and have availability for your Nike campaign." - -**Payload**: -```json -{ - "products": [ - { - "product_id": "connected_tv_prime", - "name": "Connected TV - Prime Time", - "description": "Premium CTV inventory 8PM-11PM", - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard" - } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 45.00, - "currency": "USD", - "min_spend": 10000, - "estimated_exposures": 222000, - "is_custom": false, - "brief_relevance": "Premium CTV inventory aligns with sports content request and prime time targeting" - } - ] + + + +### Guaranteed Delivery Products + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find guaranteed delivery products for measurement +const result = await testAgent.getProducts({ + brief: 'Guaranteed delivery for lift study', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, + filters: { + delivery_type: 'guaranteed', + min_exposures: 100000 + } +}); + +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} guaranteed products with 100k+ exposures`); } ``` -### Response - Retail Media Products -**Message**: "I found 3 products leveraging our pet shopper data. The syndicated Pet Category audience offers the best value at $13.50 CPM with a $10K minimum. For more precision, our Custom Competitive Conquesting audience targets shoppers buying competing brands at $18 CPM with a $50K minimum. All products include incremental sales measurement and automated daily reporting." - -**Payload**: -```json -{ - "products": [ - { - "product_id": "albertsons_pet_category_syndicated", - "name": "Pet Category Shoppers - Syndicated", - "description": "Target Albertsons shoppers who have purchased pet products in the last 90 days across offsite display and video inventory.", - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "display_300x250" - }, - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_15s_vast" - } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 13.50, - "currency": "USD", - "min_spend": 10000, - "measurement": { - "type": "incremental_sales_lift", - "attribution": "deterministic_purchase", - "window": "30_days", - "reporting": "weekly_dashboard" - }, - "reporting_capabilities": { - "available_reporting_frequencies": ["daily", "monthly"], - "expected_delay_minutes": 300, - "timezone": "America/Los_Angeles", - "supports_webhooks": true, - "available_metrics": ["impressions", "spend", "clicks", "ctr", "conversions", "viewability"] - }, - "creative_policy": { - "co_branding": "optional", - "landing_page": "must_include_retailer", - "templates_available": true - }, - "is_custom": false, - "brief_relevance": "Targets pet owners using our first-party purchase data as requested" - }, - { - "product_id": "albertsons_custom_competitive_conquest", - "name": "Custom: Competitive Dog Food Buyers", - "description": "Custom audience of Albertsons shoppers who buy competitive dog food brands. Higher precision targeting for conquest campaigns.", - "format_ids": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "display_300x250" + +```python Python +import asyncio +from adcp import test_agent + +async def discover_guaranteed(): + # Find guaranteed delivery products for measurement + result = await test_agent.simple.get_products( + brief='Guaranteed delivery for lift study', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' }, - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "display_728x90" + filters={ + 'delivery_type': 'guaranteed', + 'min_exposures': 100000 } - ], - "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 18.00, - "currency": "USD", - "min_spend": 50000, - "measurement": { - "type": "incremental_sales_lift", - "attribution": "deterministic_purchase", - "window": "30_days", - "reporting": "weekly_dashboard" - }, - "creative_policy": { - "co_branding": "required", - "landing_page": "retailer_site_only", - "templates_available": true - }, - "is_custom": true, - "expires_at": "2024-02-15T00:00:00Z", - "brief_relevance": "Precision targeting of competitive brand buyers for maximum conversion potential" - } - ] -} -``` -### Response - Clarification Needed -**Message**: "I'd be happy to help find the right sports inventory for your Nike campaign. To provide the best recommendations, could you share:\n\nβ€’ What's your campaign budget?\nβ€’ When do you want the campaign to run?\nβ€’ Which geographic markets are you targeting?\nβ€’ What are your success metrics (awareness, conversions, etc.)?" + ) + print(f"Found {len(result.products)} guaranteed products with 100k+ exposures") -**Payload**: -```json -{ - "products": [] -} +asyncio.run(discover_guaranteed()) ``` -## Policy Compliance Responses -When the promoted offering is subject to policy restrictions, the response will indicate the compliance status: -### Blocked Advertiser Category -**Message**: "I'm unable to offer products for this campaign. Our publisher policy prohibits alcohol advertising without age verification capabilities, and we don't currently support age-gated inventory. You may want to explore other publishers who specialize in age-restricted content." - -**Payload**: -```json -{ - "products": [] -} -``` -### Restricted Category (Manual Approval Required) -**Message**: "Cryptocurrency advertising requires manual approval on our platform. While I can't show available products yet, our sales team can work with you to review your campaign and potentially unlock inventory. Please reach out to sales@publisher.com to start the approval process." -**Payload**: -```json -{ - "products": [] -} -``` -## Usage Notes -- The `brief` field is optional - omit it to signal a run-of-network request -- **No brief = Run-of-network**: Publisher returns broad reach products, not the entire catalog -- Format filtering ensures advertisers only see inventory that matches their creative capabilities -- If no brief is provided, returns run-of-network products (high-volume, broad reach inventory) -- The `brief_relevance` field is only included when a brief parameter is provided -- Products represent available advertising inventory with specific targeting, format, and pricing characteristics -- The `message` field provides a human-readable summary of the response -- Publishers may request clarification when briefs are incomplete -## Brief Requirements -For comprehensive guidance on brief structure and expectations, see the [Brief Expectations](/docs/media-buy/product-discovery/brief-expectations) documentation. Key points: -- **Optional**: The `brief` field - include for recommendations, omit for run-of-network -- **Run-of-Network**: Omit brief to get broad reach products (not entire catalog) -- **Recommendations**: Include brief when you want publisher help selecting products -- **Clarification**: Publishers may request additional information when brief is provided but incomplete -Two valid approaches: -1. **No brief + filters** = Run-of-network products (broad reach inventory) -2. **Brief + objectives** = Targeted recommendations based on campaign goals -## Discovery Workflow - -**Two-Step Format Discovery**: `get_products` returns format references (IDs only), requiring `list_creative_formats` to get full specifications. - -```mermaid -graph TD - A[list_creative_formats] --> B[Get full format specifications] - B --> C[Filter products by format capabilities] - C --> D[get_products - returns format IDs only] - D --> E{Products found?} - E -->|Yes| F[Cross-reference format IDs with format specs] - E -->|No| G[Generate custom products] - F --> H[Verify creative requirements match] - G --> H - H --> I[create_media_buy] -``` -### 1. Format Discovery -Start by understanding available formats: -```javascript -// Discover audio formats for a podcast advertiser -const formats = await client.call_tool("list_creative_formats", { - type: "audio", - standard_only: true -}); -``` -### 2. Product Discovery with Format Filtering -Use format knowledge to filter products: -```javascript -// Only discover products that accept standard audio formats -const products = await client.call_tool("get_products", { - context_id: null, - brief: "Reach young adults interested in gaming", + + +### Standard Formats Only + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find products that only accept IAB standard formats +const result = await testAgent.getProducts({ + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + }, filters: { - format_types: ["audio"], standard_formats_only: true } }); -// Products return format IDs only: ["audio_standard_30s"] -``` -This prevents audio advertisers from seeing video inventory they can't use. -### 3. Product Review -The system returns matching products with all details needed for decision-making: -- Product specifications -- Pricing information -- Available targeting -- Format references (use `list_creative_formats` for full creative requirements) -### 4. Custom Product Generation -For unique requirements, systems can implement custom product generation, returning products with `is_custom: true`. -## Implementation Guide -### Step 1: Implement Product Catalog -Create a product catalog that represents your available inventory: -```python -def get_product_catalog(): - return [ - Product( - product_id="connected_tv_prime", - name="Connected TV - Prime Time", - description="Premium CTV inventory 8PM-11PM", - formats=["video_standard"], # Format IDs only - delivery_type="guaranteed", - is_fixed_price=True, - cpm=45.00 - ), - # Add more products... - ] + +if (result.success && result.data) { + console.log(`Found ${result.data.products.length} products with standard formats only`); +} ``` -### Step 2: Implement Natural Language Processing -The `get_products` tool needs to interpret briefs and filter products: -```python -@mcp.tool -def get_products(req: GetProductsRequest, context: Context) -> GetProductsResponse: - # Authenticate principal - principal_id = _get_principal_id_from_context(context) - # Get context - context_id = req.context_id or _create_context() - # Get all available products - all_products = get_product_catalog() - # If no brief provided, return run-of-network products - if not req.brief: - # Filter for broad reach, high-volume products - ron_products = filter_run_of_network_products(all_products, req.filters) - return GetProductsResponse( - message=f"Found {len(ron_products)} run-of-network products for maximum reach.", - context_id=context_id, - products=ron_products - ) - # Check if brief needs clarification - missing_info = analyze_brief_completeness(req.brief) - if missing_info: - questions = generate_clarification_questions(missing_info) - return GetProductsResponse( - message=questions, - context_id=context_id, - products=[] - ) - # Use AI to filter products based on brief - relevant_products = filter_products_by_brief(req.brief, all_products) - # Generate summary message - message = generate_product_summary(relevant_products, req.brief) - return GetProductsResponse( - message=message, - context_id=context_id, - products=relevant_products + +```python Python +import asyncio +from adcp import test_agent + +async def discover_standard_formats(): + # Find products that only accept IAB standard formats + result = await test_agent.simple.get_products( + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + }, + filters={ + 'standard_formats_only': True + } ) + print(f"Found {len(result.products)} products with standard formats only") + +asyncio.run(discover_standard_formats()) ``` -### Step 3: Run-of-Network Filtering -When no brief is provided, filter for broad reach products: -```python -def filter_run_of_network_products(products: List[Product], filters: dict) -> List[Product]: - """Filter for run-of-network products (broad reach, high volume)""" - ron_products = [] - for product in products: - # Check format compatibility - if not matches_format_filters(product, filters): - continue - # Check if it's a broad reach product (not niche/targeted) - if is_broad_reach_product(product): - ron_products.append(product) - # Sort by reach/scale potential (e.g., lower CPM = broader reach) - return sorted(ron_products, key=lambda p: p.cpm) -def is_broad_reach_product(product: Product) -> bool: - """Identify products suitable for run-of-network buying""" - # Examples of broad reach indicators: - # - Names like "Open Exchange", "Run of Network", "Broad Reach" - # - Lower CPMs indicating less targeting - # - Non-guaranteed/programmatic delivery - # - Large minimum impressions - broad_keywords = ["open", "exchange", "network", "broad", "reach", "scale"] - # Check product name/description for broad reach indicators - name_lower = product.name.lower() - desc_lower = product.description.lower() - for keyword in broad_keywords: - if keyword in name_lower or keyword in desc_lower: - return True - # Programmatic products are typically broader reach - if product.delivery_type == "non_guaranteed": - return True - return False -``` -### Step 4: AI-Powered Filtering and Message Generation -Implement the AI logic to match briefs to products and generate helpful messages: -```python -def analyze_brief_completeness(brief: str) -> List[str]: - """Analyze what information is missing from the brief""" - missing = [] - if "budget" not in brief.lower() and "$" not in brief: - missing.append("budget") - if not any(word in brief.lower() for word in ["when", "date", "month", "quarter"]): - missing.append("timing") - if not any(word in brief.lower() for word in ["where", "geo", "market", "location"]): - missing.append("geography") - if not any(word in brief.lower() for word in ["goal", "objective", "kpi", "metric"]): - missing.append("objectives") - return missing -def generate_clarification_questions(missing_info: List[str]) -> str: - """Generate natural language questions for missing information""" - questions = "I'd be happy to help find the right products for your campaign. To provide the best recommendations, could you share:" - question_map = { - "budget": "What's your campaign budget?", - "timing": "When do you want the campaign to run?", - "geography": "Which geographic markets are you targeting?", - "objectives": "What are your success metrics (awareness, conversions, etc.)?" - } - for info in missing_info: - questions += f"\n\nβ€’ {question_map.get(info, '')}" - return questions -def generate_product_summary(products: List[Product], brief: str) -> str: - """Generate a helpful summary of the products found""" - if not products: - return "I couldn't find any products matching your requirements. Let me know if you'd like to adjust your criteria." - if len(products) == 1: - p = products[0] - return f"I found one perfect match: {p.name} at ${p.cpm} CPM with {p.delivery_type} delivery. {p.brief_relevance}" - # Find best value and premium options - sorted_by_cpm = sorted(products, key=lambda p: p.cpm) - return f"I found {len(products)} products matching your requirements. {sorted_by_cpm[0].name} offers the best value at ${sorted_by_cpm[0].cpm} CPM, while {sorted_by_cpm[-1].name} provides premium placement at ${sorted_by_cpm[-1].cpm} CPM. All options support your campaign objectives." -``` -### Step 4: Brief Processing -Implement the AI logic to match briefs to products: -```python -def filter_products_by_brief(brief: str, products: List[Product]) -> List[Product]: - # Example implementation using an LLM - prompt = f""" - Campaign Brief: {brief} - Available Products: - {json.dumps([p.dict() for p in products], indent=2)} - Return the product IDs that best match this brief. - Consider targeting capabilities, formats, and inventory type. - """ - # Call your LLM here - matched_ids = call_llm_for_matching(prompt) - # Filter products - return [p for p in products if p.product_id in matched_ids] -``` -## Best Practices -### 1. Brief Interpretation -- **Extract Key Elements**: Parse briefs for targeting, budget, timing, and objectives -- **Handle Ambiguity**: Ask for clarification or provide multiple options -- **Learn from History**: Use past campaigns to improve matching -### 2. Product Matching -- **Multi-Factor Scoring**: Consider format, targeting, budget, and timing -- **Explain Matches**: Provide clear reasons why products were recommended via `brief_relevance` -- **Fallback Options**: Always provide alternatives if perfect matches aren't found -### 3. Performance Optimization -- **Cache Results**: Cache brief interpretations for similar queries -- **Batch Processing**: Process multiple briefs efficiently -- **Feedback Loop**: Use performance data to improve recommendations -## Principal-Specific Products -Implement principal-specific product visibility: -```python -def get_products_for_principal(principal_id: str) -> List[Product]: - # Get base catalog - products = get_product_catalog() - # Add principal-specific products - principal_products = get_principal_specific_products(principal_id) - products.extend(principal_products) - # Filter based on principal's access level - return filter_by_principal_access(products, principal_id) -``` + + + ## Error Handling -Common error scenarios and handling: -```python -@mcp.tool -def get_products(req: GetProductsRequest, context: Context) -> GetProductsResponse: - try: - principal_id = _get_principal_id_from_context(context) - except: - raise ToolError("Authentication required", code="AUTH_REQUIRED") - if req.brief and len(req.brief) > 1000: - raise ToolError("Brief too long", code="INVALID_REQUEST") - # Continue with normal processing... + +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `AUTH_REQUIRED` | Authentication needed for full catalog | Provide credentials via auth header | +| `INVALID_REQUEST` | Brief too long or malformed filters | Check request parameters | +| `POLICY_VIOLATION` | Category blocked for advertiser | See policy response message for details | + +### Authentication Comparison + +See the difference between authenticated and unauthenticated access: + + + +```javascript JavaScript +import { testAgent, testAgentNoAuth } from '@adcp/client/testing'; + +// WITH authentication - full catalog with pricing +const fullCatalog = await testAgent.getProducts({ + brief: 'Premium CTV inventory for brand awareness', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + } +}); + +console.log(`With auth: ${fullCatalog.products.length} products`); +console.log(`First product pricing: ${fullCatalog.products[0].pricing_options.length} options`); + +// WITHOUT authentication - limited public catalog +const publicCatalog = await testAgentNoAuth.getProducts({ + brief: 'Premium CTV inventory for brand awareness', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' + } +}); + +console.log(`Without auth: ${publicCatalog.products.length} products`); +console.log(`First product pricing: ${publicCatalog.products[0].pricing_options?.length || 0} options`); ``` -## Testing Discovery -Test your discovery implementation thoroughly: -```python -# Test various brief styles -test_briefs = [ - "video ads for millennials", - "reach pet owners in California with CTV", - "low budget display campaign", - "premium sports inventory during playoffs" -] -for brief in test_briefs: - result = get_products(GetProductsRequest(brief=brief), context) - assert len(result.products) > 0 - print(f"Brief: {brief} -> Found {len(result.products)} products") + +```python Python +import asyncio +from adcp import test_agent, test_agent_no_auth + +async def compare_auth(): + # WITH authentication - full catalog with pricing + full_catalog = await test_agent.simple.get_products( + brief='Premium CTV inventory for brand awareness', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } + ) + + print(f"With auth: {len(full_catalog['products'])} products") + print(f"First product pricing: {len(full_catalog['products'][0]['pricing_options'])} options") + + # WITHOUT authentication - limited public catalog + public_catalog = await test_agent_no_auth.simple.get_products( + brief='Premium CTV inventory for brand awareness', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } + ) + + print(f"Without auth: {len(public_catalog['products'])} products") + print(f"First product pricing: {len(public_catalog['products'][0].get('pricing_options', []))} options") + +asyncio.run(compare_auth()) ``` -## Integration with Media Buy Flow -Discovery is just the first step. Ensure smooth transitions to the next phases: -1. **Discovery** β†’ `get_products` finds relevant inventory -2. **Purchase** β†’ [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) executes the campaign -3. **Creative** β†’ [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) uploads assets -4. **Monitor** β†’ Track delivery and optimize \ No newline at end of file + + + +**Key Differences:** +- **Product Count**: Authenticated access returns more products, including private/custom offerings +- **Pricing Information**: Only authenticated requests receive detailed pricing options (CPM, CPCV, etc.) +- **Targeting Details**: Custom targeting capabilities may be restricted to authenticated users +- **Rate Limits**: Unauthenticated requests have lower rate limits + +## Authentication Behavior + +- **Without credentials**: Returns limited catalog (run-of-network products), no pricing, no custom offerings +- **With credentials**: Returns complete catalog with pricing and custom products + +See [Authentication Guide](/docs/reference/authentication) for details. + +## Next Steps + +After discovering products: + +1. **Review Options**: Compare products, pricing, and targeting capabilities +2. **Create Media Buy**: Use [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) to execute campaign +3. **Prepare Creatives**: Use [`list_creative_formats`](/docs/media-buy/task-reference/list_creative_formats) to see format requirements +4. **Upload Assets**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) to provide creative assets + +## Learn More + +- [Product Discovery Guide](/docs/media-buy/product-discovery/) - Understanding briefs and products +- [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) - CPM, CPCV, CPP explained +- [Brief Expectations](/docs/media-buy/product-discovery/brief-expectations) - How to write effective briefs +- [Media Products](/docs/media-buy/product-discovery/media-products) - Product structure and fields diff --git a/docs/media-buy/task-reference/list_authorized_properties.mdx b/docs/media-buy/task-reference/list_authorized_properties.mdx index 6de6261c..cd74bf7e 100644 --- a/docs/media-buy/task-reference/list_authorized_properties.mdx +++ b/docs/media-buy/task-reference/list_authorized_properties.mdx @@ -5,17 +5,17 @@ sidebar_position: 1.5 # list_authorized_properties -Discover which publishers this sales agent is authorized to represent, similar to IAB Tech Lab's sellers.json. Returns just publisher domains. Buyers fetch each publisher's adagents.json to see property definitions and verify authorization scope. +Discover which publishers a sales agent is authorized to represent. Returns publisher domains only - buyers fetch full property definitions from each publisher's adagents.json. **Response Time**: ~2 seconds (database lookup) **Purpose**: - Authorization discovery - which publishers does this agent represent? -- Single source of truth - all details (properties, authorization scope) come from publisher's adagents.json +- Single source of truth - property definitions come from publisher's adagents.json - One-time discovery to cache publisher-agent relationships -**Request Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-request.json) -**Response Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-response.json) +**Request Schema**: [`/schemas/v1/media-buy/list-authorized-properties-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-request.json) +**Response Schema**: [`/schemas/v1/media-buy/list-authorized-properties-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-response.json) ## Request Parameters @@ -23,349 +23,427 @@ Discover which publishers this sales agent is authorized to represent, similar t |-----------|------|----------|-------------| | `publisher_domains` | string[] | No | Filter to specific publisher domains (e.g., `["cnn.com", "espn.com"]`) | -## Response (Message) +## Response -The response includes a human-readable message that: -- Summarizes the number of publishers represented -- Lists publisher domains -- Notes any filtering applied +| Field | Description | +|-------|-------------| +| `publisher_domains` | Array of publisher domains this agent represents | +| `primary_channels` | Optional main advertising channels (ctv, display, video, audio, dooh, etc.) | +| `primary_countries` | Optional main countries (ISO 3166-1 alpha-2 codes) | +| `portfolio_description` | Optional markdown description of portfolio and capabilities | +| `last_updated` | Optional ISO 8601 timestamp of last publisher list update | -The message is returned differently in each protocol: -- **MCP**: Returned as a `message` field in the JSON response -- **A2A**: Returned as a text part in the artifact +See [schema](https://adcontextprotocol.org/schemas/v1/media-buy/list-authorized-properties-response.json) for complete field list. -## Response (Payload) +## Authorization Workflow -```json -{ - "publisher_domains": ["cnn.com", "espn.com", "nytimes.com"], - "primary_channels": ["ctv", "display"], - "primary_countries": ["US"], - "portfolio_description": "CTV specialist representing major news and sports publishers across US markets.", - "last_updated": "2025-01-15T14:30:00Z" -} -``` +This tool is the first step in understanding what a sales agent represents: -### Field Descriptions +1. **Discovery**: Buyer calls `list_authorized_properties()` to get publisher domains +2. **Fetch Details**: Buyer fetches each publisher's `https://publisher.com/.well-known/adagents.json` +3. **Validate**: Buyer verifies agent is in publisher's `authorized_agents` array +4. **Resolve Scope**: Buyer resolves authorization scope (property_ids, property_tags, or all properties) +5. **Cache**: Buyer caches properties for future product validation -- **publisher_domains**: Array of publisher domains this agent represents. Buyers should fetch each publisher's adagents.json to: - - See property definitions - - Verify this agent is in their authorized_agents list - - Check authorization scope (property_ids, property_tags, or all properties) -- **primary_channels** *(optional)*: Main advertising channels (see [Channels enum](https://adcontextprotocol.org/schemas/v1/enums/channels.json)) -- **primary_countries** *(optional)*: Main countries (ISO 3166-1 alpha-2 codes) -- **portfolio_description** *(optional)*: Markdown description of the agent's portfolio and capabilities -- **advertising_policies** *(optional)*: Agent's policies and restrictions (publisher-specific policies come from publisher's adagents.json) -- **last_updated** *(optional)*: ISO 8601 timestamp when the agent's publisher list was last updated. Buyers can compare this to cached publisher adagents.json timestamps to detect staleness. +### Key Insight: Publishers Own Property Definitions -## Workflow: From Authorization to Property Details +Unlike traditional SSPs: +- **Publishers** define properties in their own `adagents.json` file +- **Sales agents** reference those definitions via domain list +- **Buyers** fetch property details from publishers, not agents +- This ensures single source of truth and prevents property definition drift -This tool is the first step in understanding what a sales agent represents: +## Common Scenarios -```mermaid -sequenceDiagram - participant Buyer as Buyer Agent - participant Sales as Sales Agent - participant Publisher as Publisher (cnn.com) +### Discover Agent Portfolio - Note over Buyer: Discovery Phase - Buyer->>Sales: list_authorized_properties() - Sales-->>Buyer: publisher_domains: ["cnn.com", "espn.com"] + - Note over Buyer: Fetch Property Details - Buyer->>Publisher: GET https://cnn.com/.well-known/adagents.json - Publisher-->>Buyer: {properties: [...], tags: {...}, authorized_agents: [...]} +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; - Note over Buyer: Validate Authorization - Buyer->>Buyer: Find sales agent in publisher's authorized_agents array - Buyer->>Buyer: Check authorization scope (property_ids, property_tags, or all) - Buyer->>Buyer: Resolve scope to actual property list +// Get all authorized publishers +const result = await testAgent.listAuthorizedProperties(); - Note over Buyer: Cache for Future Use - Buyer->>Buyer: Cache publisher properties + agent authorization +console.log(`Agent represents ${result.publisher_domains.length} publishers`); +console.log(`Primary channels: ${result.primary_channels?.join(', ')}`); +console.log(`Countries: ${result.primary_countries?.join(', ')}`); - Note over Buyer: Product Discovery - Buyer->>Sales: get_products(...) - Sales-->>Buyer: Products referencing properties - Buyer->>Buyer: Use cached publisher properties for validation +result.publisher_domains.forEach(domain => { + console.log(`- ${domain}`); +}); ``` -### Key Insight: Publishers Own Property Definitions +```python Python +from adcp.test_helpers import test_agent -Unlike traditional supply-side platforms where the SSP defines properties, in AdCP: -- **Publishers** define their properties in their own `adagents.json` file -- **Sales agents** reference those definitions via `list_authorized_properties` -- **Buyers** fetch property details from publishers, not from sales agents -- This ensures a single source of truth and prevents property definition drift +# Get all authorized publishers +result = test_agent.list_authorized_properties() -## Protocol-Specific Examples +print(f"Agent represents {len(result['publisher_domains'])} publishers") +print(f"Primary channels: {', '.join(result.get('primary_channels', []))}") +print(f"Countries: {', '.join(result.get('primary_countries', []))}") -The AdCP payload is identical across protocols. Only the request/response wrapper differs. +for domain in result['publisher_domains']: + print(f"- {domain}") +``` -### MCP Request -```json -{ - "tool": "list_authorized_properties", - "arguments": { - "publisher_domains": ["cnn.com"] - } + + +### Filter by Publisher + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Check if agent represents specific publishers +const result = await testAgent.listAuthorizedProperties({ + publisher_domains: ['cnn.com', 'espn.com'] +}); + +if (result.publisher_domains.length > 0) { + console.log(`Agent represents ${result.publisher_domains.length} of requested publishers`); + result.publisher_domains.forEach(domain => { + console.log(`βœ“ ${domain}`); + }); +} else { + console.log('Agent does not represent any of the requested publishers'); } ``` -### MCP Response -```json -{ - "message": "Authorized to represent 3 publishers: cnn.com, espn.com, nytimes.com", - "publisher_domains": ["cnn.com", "espn.com", "nytimes.com"], - "primary_channels": ["ctv"], - "primary_countries": ["US"] -} +```python Python +from adcp.test_helpers import test_agent + +# Check if agent represents specific publishers +result = test_agent.list_authorized_properties( + publisher_domains=['cnn.com', 'espn.com'] +) + +if result['publisher_domains']: + print(f"Agent represents {len(result['publisher_domains'])} of requested publishers") + for domain in result['publisher_domains']: + print(f"βœ“ {domain}") +else: + print('Agent does not represent any of the requested publishers') ``` -### A2A Request -```javascript -await a2a.send({ - message: { - parts: [ - { - kind: "data", - data: { - skill: "list_authorized_properties", - parameters: { - publisher_domains: ["cnn.com"] - } - } + + +### Fetch Publisher Property Definitions + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Step 1: Get authorized publishers +const authResult = await testAgent.listAuthorizedProperties(); + +// Step 2: Fetch property definitions from each publisher +const publisherProperties = {}; + +for (const domain of authResult.publisher_domains) { + try { + const response = await fetch(`https://${domain}/.well-known/adagents.json`); + const adagents = await response.json(); + + // Find this agent in publisher's authorized list + const agentAuth = adagents.authorized_agents.find( + a => a.url === 'https://test-agent.adcontextprotocol.org/mcp' + ); + + if (agentAuth) { + // Resolve authorized properties based on scope + let properties; + if (agentAuth.property_ids) { + properties = adagents.properties.filter( + p => agentAuth.property_ids.includes(p.property_id) + ); + } else if (agentAuth.property_tags) { + properties = adagents.properties.filter( + p => p.tags?.some(tag => agentAuth.property_tags.includes(tag)) + ); + } else { + properties = adagents.properties; // All properties } - ] + + publisherProperties[domain] = properties; + console.log(`${domain}: ${properties.length} properties authorized`); + } + } catch (error) { + console.error(`Failed to fetch ${domain}: ${error.message}`); } -}); +} ``` -### A2A Response -```json -{ - "artifacts": [{ - "name": "authorized_properties_result", - "parts": [ - { - "kind": "text", - "text": "Authorized to represent 3 publishers: cnn.com, espn.com, nytimes.com" - }, - { - "kind": "data", - "data": { - "publisher_domains": ["cnn.com", "espn.com", "nytimes.com"], - "primary_channels": ["ctv"], - "primary_countries": ["US"] - } - } - ] - }] -} +```python Python +from adcp.test_helpers import test_agent +import requests + +# Step 1: Get authorized publishers +auth_result = test_agent.list_authorized_properties() + +# Step 2: Fetch property definitions from each publisher +publisher_properties = {} + +for domain in auth_result['publisher_domains']: + try: + response = requests.get(f"https://{domain}/.well-known/adagents.json") + adagents = response.json() + + # Find this agent in publisher's authorized list + agent_auth = next( + (a for a in adagents['authorized_agents'] if a['url'] == 'https://test-agent.adcontextprotocol.org/mcp'), + None + ) + + if agent_auth: + # Resolve authorized properties based on scope + if 'property_ids' in agent_auth: + properties = [p for p in adagents['properties'] + if p['property_id'] in agent_auth['property_ids']] + elif 'property_tags' in agent_auth: + properties = [p for p in adagents['properties'] + if any(tag in agent_auth['property_tags'] + for tag in p.get('tags', []))] + else: + properties = adagents['properties'] # All properties + + publisher_properties[domain] = properties + print(f"{domain}: {len(properties)} properties authorized") + except Exception as error: + print(f"Failed to fetch {domain}: {error}") ``` -## Property Portfolio Metadata + -Optional top-level fields provide high-level metadata about the property portfolio to help buying agents quickly determine relevance without examining every property. +### Check Authorization Scope -### Why Portfolio Metadata? + -**The core insight**: This isn't about what the agent *can do* (that's in A2A skills) - it's about what properties the agent *represents*. Properties change over time as inventory is added or removed. +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -**Use case**: Orchestrator needs to route brief "DOOH in US airports" to relevant agents: -```javascript -// Quick filtering before detailed analysis -const response = await agent.send({ skill: 'list_authorized_properties' }); +// Determine what type of agent this is based on portfolio +const result = await testAgent.listAuthorizedProperties(); -if (response.primary_channels?.includes('dooh') && - response.primary_countries?.includes('US')) { - // Relevant! Now examine detailed properties - const airportProperties = response.properties.filter(p => - p.tags?.includes('airports') - ); +// Check for CTV specialists +if (result.primary_channels?.includes('ctv')) { + console.log('CTV specialist'); +} + +// Check geographic focus +if (result.primary_countries?.includes('US')) { + console.log('US market focus'); +} + +// Check for multi-channel capability +if (result.primary_channels && result.primary_channels.length > 2) { + console.log(`Multi-channel agent (${result.primary_channels.join(', ')})`); +} + +// Read portfolio description +if (result.portfolio_description) { + console.log(`\nAbout: ${result.portfolio_description}`); } ``` -### Portfolio Fields +```python Python +from adcp.test_helpers import test_agent -**`primary_channels`** *(optional)*: Main advertising channels in this portfolio -- Values: `"display"`, `"video"`, `"dooh"`, `"ctv"`, `"podcast"`, `"retail"`, etc. -- See [Channels enum](https://adcontextprotocol.org/schemas/v1/enums/channels.json) for full list -- Helps filter "Do you have DOOH?" before examining properties +# Determine what type of agent this is based on portfolio +result = test_agent.list_authorized_properties() -**`primary_countries`** *(optional)*: Main countries (ISO 3166-1 alpha-2 codes) -- Where the bulk of properties are concentrated -- Helps filter "Do you have US inventory?" before examining properties +# Check for CTV specialists +if 'ctv' in result.get('primary_channels', []): + print('CTV specialist') -**`portfolio_description`** *(optional)*: Markdown description of the portfolio -- Inventory types and characteristics -- Audience profiles -- Special features or capabilities +# Check geographic focus +if 'US' in result.get('primary_countries', []): + print('US market focus') -### Example Portfolio Metadata +# Check for multi-channel capability +channels = result.get('primary_channels', []) +if len(channels) > 2: + print(f"Multi-channel agent ({', '.join(channels)})") -**DOOH Network**: -```json -{ - "primary_channels": ["dooh"], - "primary_countries": ["US", "CA"], - "portfolio_description": "Premium digital out-of-home across airports and transit. Business traveler focus with proof-of-play." -} +# Read portfolio description +if result.get('portfolio_description'): + print(f"\nAbout: {result['portfolio_description']}") ``` -**Multi-Channel Publisher**: -```json -{ - "primary_channels": ["display", "video", "native"], - "primary_countries": ["US", "GB", "AU"], - "portfolio_description": "News and business publisher network. Desktop and mobile web properties with professional audience." -} -``` + -**Large Radio Network**: -```json -{ - "primary_channels": ["audio"], - "primary_countries": ["US"], - "portfolio_description": "National radio network covering all US DMAs. Mix of news, talk, and music formats." -} -``` +### Cache Validation with last_updated -## Implementation Guide for Sales Agents + -Sales agents should return publisher authorizations that match their authorization in publisher adagents.json files: +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -### Step 1: Read Own Authorization +// Use last_updated to determine if cache is stale +const result = await testAgent.listAuthorizedProperties(); -From agent's own `adagents.json` `publisher_properties` entries, extract: -- Publisher domains represented -- Authorization scope (property_ids or property_tags for each publisher) +// Example cache from previous fetch (in practice, load from storage) +const cache = { + 'example-publisher.com': { + last_updated: '2024-01-15T10:00:00Z', + properties: [] + } +}; -### Step 2: Return Publisher Domain List +for (const domain of result.publisher_domains) { + const cached = cache[domain]; -Return just the list of publisher domains: + if (cached && result.last_updated) { + const cachedDate = new Date(cached.last_updated); + const agentDate = new Date(result.last_updated); -```json -{ - "publisher_domains": ["cnn.com", "espn.com", "nytimes.com"] + if (cachedDate >= agentDate) { + console.log(`${domain}: Using cached data (still fresh)`); + continue; + } + } + + console.log(`${domain}: Fetching updated property definitions`); + // Fetch from publisher's adagents.json... } ``` -**That's it.** You don't need to: -- Specify authorization scope (buyers will find that in publisher's adagents.json) -- Fetch publisher adagents.json files (buyers will do that) -- Resolve property IDs to full property objects -- Duplicate property definitions -- Keep property data in sync +```python Python +from adcp.test_helpers import test_agent +from datetime import datetime -### Step 3: Portfolio Metadata (Optional) +# Use last_updated to determine if cache is stale +result = test_agent.list_authorized_properties() -Add high-level metadata about your capabilities: -```json -{ - "publisher_domains": ["cnn.com", "espn.com"], - "primary_channels": ["ctv"], - "primary_countries": ["US"], - "portfolio_description": "CTV specialist for news and sports publishers" +# Example cache from previous fetch (in practice, load from storage) +cache = { + 'example-publisher.com': { + 'last_updated': '2024-01-15T10:00:00Z', + 'properties': [] + } } -``` -## Implementation Guide for Buyer Agents +for domain in result['publisher_domains']: + cached = cache.get(domain) -Buyer agents should use this tool to discover which publishers an agent represents, then fetch property details from publishers. + if cached and result.get('last_updated'): + cached_date = datetime.fromisoformat(cached['last_updated']) + agent_date = datetime.fromisoformat(result['last_updated']) -### Step 1: Call list_authorized_properties + if cached_date >= agent_date: + print(f"{domain}: Using cached data (still fresh)") + continue -```javascript -const response = await salesAgent.listAuthorizedProperties(); -// Returns: {publisher_domains: ["cnn.com", "espn.com", "nytimes.com"]} + print(f"{domain}: Fetching updated property definitions") + # Fetch from publisher's adagents.json... ``` -### Step 2: Fetch Publisher Property Definitions + -```javascript -for (const publisherDomain of response.publisher_domains) { - // Check cache freshness using last_updated - const cached = cache.get(publisherDomain); - if (cached && response.last_updated) { - const cachedTimestamp = new Date(cached.last_updated); - const agentTimestamp = new Date(response.last_updated); +## Portfolio Metadata - if (cachedTimestamp >= agentTimestamp) { - // Cache is still fresh - continue; - } - } +Optional fields provide high-level portfolio information for quick filtering: - // Fetch publisher's canonical adagents.json - const publisherAgents = await fetch( - `https://${publisherDomain}/.well-known/adagents.json` - ).then(r => r.json()); +### primary_channels - // Find agent's authorization entry in publisher's file - const agentAuth = publisherAgents.authorized_agents.find( - a => a.url === salesAgentUrl - ); +Main advertising channels in portfolio: +- Values: `display`, `video`, `dooh`, `ctv`, `podcast`, `retail`, etc. +- See [Channels enum](https://adcontextprotocol.org/schemas/v1/enums/channels.json) +- Use case: Filter "Do you have DOOH?" before examining properties - if (!agentAuth) { - console.warn(`Agent not found in ${publisherDomain} authorized_agents`); - continue; - } +### primary_countries - // Resolve property scope from publisher's authorization - let authorizedProperties; - if (agentAuth.property_ids) { - authorizedProperties = publisherAgents.properties.filter( - p => agentAuth.property_ids.includes(p.property_id) - ); - } else if (agentAuth.property_tags) { - authorizedProperties = publisherAgents.properties.filter( - p => p.tags?.some(tag => agentAuth.property_tags.includes(tag)) - ); - } else { - // No scope = all properties - authorizedProperties = publisherAgents.properties; - } +Main countries (ISO 3166-1 alpha-2): +- Where bulk of properties are concentrated +- Use case: Filter "Do you have US inventory?" - // Cache for use in product validation - cache.set(publisherDomain, { - properties: authorizedProperties, - tags: publisherAgents.tags, - last_updated: publisherAgents.last_updated || new Date().toISOString() - }); -} -``` +### portfolio_description -### Step 3: Use Cached Properties +Markdown description: +- Inventory types and characteristics +- Audience profiles +- Special features or capabilities -When validating products: -```javascript -// Product references properties -const product = await salesAgent.getProducts(...); +### Example Portfolios -for (const property of product.properties) { - const cached = cache.get(property.publisher_domain); - // Validate against cached publisher definitions +**DOOH Network**: +```json +{ + "primary_channels": ["dooh"], + "primary_countries": ["US", "CA"], + "portfolio_description": "Premium digital out-of-home across airports and transit. Business traveler focus with proof-of-play." +} +``` + +**News Publisher**: +```json +{ + "primary_channels": ["display", "video", "native"], + "primary_countries": ["US", "GB", "AU"], + "portfolio_description": "News and business publisher network. Desktop and mobile web with professional audience." } ``` ## Use Cases ### Third-Party Sales Networks -A CTV sales network representing multiple publishers: -- Returns list of publisher domains and authorization scope +CTV network representing multiple publishers: +- Returns list of publisher domains - Buyers fetch property details from each publisher - No duplication of property data across agents ### Publisher Direct Sales -A publisher selling their own inventory: -- Returns their own domain with authorization scope -- Buyers fetch property definitions from publisher's adagents.json -- Consistent with how third-party agents work +Publisher selling own inventory: +- Returns own domain +- Buyers fetch from publisher's adagents.json +- Consistent with third-party agent flow ### Authorization Validation -Buyer agents validating seller authorization: +Buyer validating seller authorization: - Discover which publishers agent claims to represent -- Fetch each publisher's adagents.json to verify authorization -- Check agent URL is in publisher's authorized_agents list -- Cache validated relationships \ No newline at end of file +- Fetch each publisher's adagents.json to verify +- Check agent URL in authorized_agents list +- Cache validated relationships + +## Error Handling + +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `INVALID_REQUEST` | Invalid publisher_domains parameter | Check domain format | +| `NO_PUBLISHERS` | Agent represents no publishers | Agent may be misconfigured | + +## Best Practices + +**1. Cache Publisher Property Definitions** +Fetch once and cache - properties rarely change. Use `last_updated` to detect staleness. + +**2. Validate Authorization from Publisher** +Always verify agent is in publisher's `authorized_agents` array - don't trust agent claims alone. + +**3. Resolve Authorization Scope** +Check property_ids, property_tags, or assume all properties based on publisher's authorization entry. + +**4. Use Portfolio Metadata for Filtering** +Check `primary_channels` and `primary_countries` before fetching detailed properties. + +**5. Handle Fetch Failures Gracefully** +Publishers may be temporarily unavailable - cache and retry with backoff. + +## Next Steps + +After discovering authorized properties: + +1. **Fetch Properties**: GET `https://publisher.com/.well-known/adagents.json` +2. **Validate Authorization**: Find agent in publisher's `authorized_agents` array +3. **Cache Properties**: Store for use in product validation +4. **Discover Products**: Use [`get_products`](/docs/media-buy/task-reference/get_products) with cached property context + +## Learn More + +- [adagents.json Specification](/docs/media-buy/capability-discovery/adagents) - Publisher authorization file format +- [Property Schema](https://adcontextprotocol.org/schemas/v1/core/property.json) - Property definition structure +- [Authorization Guide](/docs/reference/authentication) - How authorization works in AdCP diff --git a/docs/media-buy/task-reference/list_creative_formats.mdx b/docs/media-buy/task-reference/list_creative_formats.mdx index 0762b995..9e27e6e0 100644 --- a/docs/media-buy/task-reference/list_creative_formats.mdx +++ b/docs/media-buy/task-reference/list_creative_formats.mdx @@ -5,681 +5,423 @@ sidebar_position: 2 # list_creative_formats -Discover all creative formats supported by this agent. Returns full format definitions, not just IDs. +Discover creative formats supported by a sales or creative agent. Returns full format specifications including asset requirements and technical constraints. -**Response Time**: ~1 second (simple database lookup) +**Response Time**: ~1 second (database lookup) -**Authentication**: None required - this endpoint must be publicly accessible for format discovery +**Authentication**: None required (public endpoint for format discovery) -**Request Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-request.json) -**Response Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-response.json) - -## Recursive Discovery Model - -Both sales agents and creative agents use the same response format: -1. **formats**: Full format definitions for formats they own/support -2. **creative_agents** (optional): URLs to other creative agents providing additional formats - -Each format includes an **agent_url** field indicating its authoritative source. - -Buyers can recursively query creative_agents to discover all available formats. **Buyers must track visited URLs to avoid infinite loops.** +**Request Schema**: [`/schemas/v1/media-buy/list-creative-formats-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-request.json) +**Response Schema**: [`/schemas/v1/media-buy/list-creative-formats-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/list-creative-formats-response.json) ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `format_ids` | FormatID[] | No | Return only these specific structured format ID objects (e.g., from `get_products` response) | -| `type` | string | No | Filter by format type: `"audio"`, `"video"`, `"display"`, `"dooh"` (technical categories with distinct requirements) | -| `asset_types` | string[] | No | Filter to formats that include these asset types. For third-party tags, search for `["html"]` or `["javascript"]`. E.g., `["image", "text"]` returns formats with images and text, `["javascript"]` returns formats accepting JavaScript tags. Values: `image`, `video`, `audio`, `text`, `html`, `javascript`, `url` | -| `max_width` | integer | No | Maximum width in pixels (inclusive). Returns formats where **any render** has width ≀ this value. For multi-render formats (e.g., video with companion banner), matches if at least one render fits. | -| `max_height` | integer | No | Maximum height in pixels (inclusive). Returns formats where **any render** has height ≀ this value. For multi-render formats, matches if at least one render fits. | -| `min_width` | integer | No | Minimum width in pixels (inclusive). Returns formats where **any render** has width β‰₯ this value. | -| `min_height` | integer | No | Minimum height in pixels (inclusive). Returns formats where **any render** has height β‰₯ this value. | -| `is_responsive` | boolean | No | Filter for responsive formats that adapt to container size. When `true`, returns formats without fixed dimensions. | -| `name_search` | string | No | Search for formats by name (case-insensitive partial match, e.g., `"mobile"` or `"vertical"`) | +| `format_ids` | FormatID[] | No | Return only specific format IDs (from `get_products` response) | +| `type` | string | No | Filter by type: `audio`, `video`, `display`, `dooh` | +| `asset_types` | string[] | No | Filter to formats accepting these asset types: `image`, `video`, `audio`, `text`, `html`, `javascript`, `url`. Uses OR logic. | +| `max_width` | integer | No | Maximum width in pixels (inclusive) - matches if ANY render fits | +| `max_height` | integer | No | Maximum height in pixels (inclusive) - matches if ANY render fits | +| `min_width` | integer | No | Minimum width in pixels (inclusive) | +| `min_height` | integer | No | Minimum height in pixels (inclusive) | +| `is_responsive` | boolean | No | Filter for responsive formats (adapt to container size) | +| `name_search` | string | No | Search formats by name (case-insensitive partial match) | ### Multi-Render Dimension Filtering -Formats may produce multiple rendered pieces (e.g., video + companion banner, desktop + mobile variants). Dimension filters use **"any render fits"** logic: +Formats may produce multiple rendered pieces (e.g., video + companion banner). Dimension filters use **"any render fits"** logic: + +- `max_width: 300, max_height: 250` - Returns formats where AT LEAST ONE render is ≀ 300Γ—250 +- Use case: "Find formats that can render into my 300Γ—250 ad slot" +- Example: Format with primary video (1920Γ—1080) + companion banner (300Γ—250) **matches** because companion fits + +## Response + +| Field | Description | +|-------|-------------| +| `formats` | Array of full format definitions (format_id, name, type, requirements, assets_required, renders) | +| `creative_agents` | Optional array of other creative agents providing additional formats | -- **`max_width: 300, max_height: 250`** - Returns formats where AT LEAST ONE render is ≀ 300Γ—250 -- **Use case**: "Find formats that can render into my 300Γ—250 ad slot" -- **Example**: A format with primary video (1920Γ—1080) + companion banner (300Γ—250) **matches** because the companion fits +See [Format schema](https://adcontextprotocol.org/schemas/v1/core/format.json) for complete format object structure. -This ensures you discover all formats capable of rendering into your available placement dimensions, even if they also include larger companion pieces. +### Recursive Discovery -## Response Structure +Sales agents may reference creative agents that provide additional formats: ```json { - "formats": [ - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "video_standard_30s" - }, - "name": "Standard Video - 30 seconds", - "type": "video", - "requirements": { /* ... */ }, - "assets_required": [ /* ... */ ] - }, - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "display_300x250" - }, - "name": "Medium Rectangle Banner", - "type": "display" - // ... full format details - } - ], - "creative_agents": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "agent_name": "AdCP Reference Creative Agent", - "capabilities": ["validation", "assembly", "preview"] - }, - { - "agent_url": "https://dco.example.com", - "agent_name": "Custom DCO Platform", - "capabilities": ["validation", "assembly", "generation", "preview"] - } - ] + "creative_agents": [{ + "agent_url": "https://creative.adcontextprotocol.org", + "agent_name": "AdCP Reference Creative Agent", + "capabilities": ["validation", "assembly", "preview"] + }] } ``` -### Field Descriptions - -- **formats**: Full format definitions for formats this agent owns/supports - - **format_id**: Unique identifier - - **agent_url**: Authoritative source URL for this format (where it's defined) - - All other format fields as per [Format schema](https://adcontextprotocol.org/schemas/v1/core/format.json) -- **creative_agents** (optional): Other creative agents providing additional formats - - **agent_url**: Base URL to query for more formats (call list_creative_formats) - - **agent_name**: Human-readable name - - **capabilities**: What the agent can do (validation/assembly/generation/preview) +Buyers can recursively query creative_agents. **Track visited URLs to avoid infinite loops.** +## Common Scenarios -## Protocol-Specific Examples +### Get Specs for Product Format IDs -The AdCP payload is identical across protocols. Only the request/response wrapper differs. + -### Example 1: Find Formats by Asset Types +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; -"I have images and text - what can I build?" - -#### MCP Request -```json -{ - "tool": "list_creative_formats", - "arguments": { - "asset_types": ["image", "text"] - } -} -``` - -#### Response -```json -{ - "formats": [ +// Get full specs for formats returned by get_products +const result = await testAgent.listCreativeFormats({ + format_ids: [ { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "display_300x250" - }, - "name": "Medium Rectangle", - "type": "display", - "dimensions": "300x250", - "assets_required": [ - { - "asset_id": "banner_image", - "asset_type": "image", - "asset_role": "hero_image", - "required": true, - "width": 300, - "height": 250, - "acceptable_formats": ["jpg", "png", "gif"], - "max_file_size_kb": 200 - }, - { - "asset_id": "headline", - "asset_type": "text", - "asset_role": "headline", - "required": true, - "max_length": 25 - } - ] + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'video_15s_hosted' }, { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "native_responsive" - }, - "name": "Responsive Native Ad", - "type": "display", - "assets_required": [ - { - "asset_id": "primary_image", - "asset_type": "image", - "asset_role": "hero_image", - "required": true - }, + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'display_300x250' + } + ] +}); + +result.formats.forEach(format => { + console.log(`${format.name}: ${format.assets_required.length} assets required`); +}); +``` + +```python Python +from adcp import test_agent + +# Get full specs for formats returned by get_products +result = test_agent.list_creative_formats( + format_ids=[ { - "asset_id": "headline", - "asset_type": "text", - "asset_role": "headline", - "required": true, - "max_length": 80 + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'video_15s_hosted' }, { - "asset_id": "description", - "asset_type": "text", - "asset_role": "body_text", - "required": false, - "max_length": 200 + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'display_300x250' } - ] - } - ] -} + ] +) + +for format in result['formats']: + print(f"{format['name']}: {len(format['assets_required'])} assets required") ``` -### Example 2: Find Formats for Third-Party JavaScript Tags + -"I have 300x250 JavaScript tags - which formats support them?" +### Find Formats by Asset Types -#### MCP Request -```json -{ - "tool": "list_creative_formats", - "arguments": { - "asset_types": ["javascript"], - "dimensions": "300x250" - } -} -``` + -#### Response -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "display_300x250_3p" - }, - "name": "Medium Rectangle - Third Party", - "type": "display", - "dimensions": "300x250", - "assets_required": [ - { - "asset_id": "tag", - "asset_type": "javascript", - "asset_role": "third_party_tag", - "required": true, - "requirements": { - "width": 300, - "height": 250, - "max_file_size_kb": 200 - } - } - ] - } - ] -} +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find formats that accept images and text +const result = await testAgent.listCreativeFormats({ + asset_types: ['image', 'text'] +}); + +console.log(`Found ${result.formats.length} formats`); + +// Examine asset requirements +result.formats.forEach(format => { + console.log(`\n${format.name}:`); + format.assets_required.forEach(asset => { + console.log(` - ${asset.asset_role}: ${asset.asset_type}`); + }); +}); ``` -### Example 3: Find Formats by Size +```python Python +from adcp import test_agent -"What formats can accept HTML, JavaScript, or images up to 970x250?" +# Find formats that accept images and text +result = test_agent.list_creative_formats( + asset_types=['image', 'text'] +) -**Important**: The `asset_types` parameter uses OR logic - formats matching ANY of the specified asset types will be returned. +print(f"Found {len(result['formats'])} formats") -#### MCP Request -```json -{ - "tool": "list_creative_formats", - "arguments": { - "asset_types": ["html", "javascript", "image"], - "max_width": 970, - "max_height": 250, - "type": "display" - } -} +# Examine asset requirements +for format in result['formats']: + print(f"\n{format['name']}:") + for asset in format['assets_required']: + print(f" - {asset['asset_role']}: {asset['asset_type']}") ``` -This query can be sent to either: -1. **Sales agent** - Returns formats the sales agent supports directly -2. **Reference creative agent** (`https://creative.adcontextprotocol.org`) - Returns all standard formats matching the criteria + -The response includes all display formats at or below 970Γ—250 that accept any of those asset types (e.g., 300Γ—250, 728Γ—90, 970Γ—250). +### Find Third-Party Tag Formats -**Example: Find responsive formats** + -```json -{ - "tool": "list_creative_formats", - "arguments": { - "is_responsive": true, - "type": "display" +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find formats that accept JavaScript or HTML tags +const result = await testAgent.listCreativeFormats({ + asset_types: ['javascript', 'html'], + max_width: 970, + max_height: 250 +}); + +console.log(`Found ${result.formats.length} third-party tag formats ≀ 970Γ—250`); + +result.formats.forEach(format => { + const renders = format.renders || []; + if (renders.length > 0) { + const dims = renders[0].dimensions; + console.log(`${format.name}: ${dims.width}Γ—${dims.height}`); } -} +}); ``` -Returns formats that adapt to container width (native ads, fluid layouts, full-width banners). +```python Python +from adcp import test_agent -### Example 4: Search by Name +# Find formats that accept JavaScript or HTML tags +result = test_agent.list_creative_formats( + asset_types=['javascript', 'html'], + max_width=970, + max_height=250 +) -"Show me mobile or vertical formats" +print(f"Found {len(result['formats'])} third-party tag formats ≀ 970Γ—250") -#### MCP Request -```json -{ - "tool": "list_creative_formats", - "arguments": { - "name_search": "vertical" - } -} +for format in result['formats']: + renders = format.get('renders', []) + if renders: + dims = renders[0]['dimensions'] + print(f"{format['name']}: {dims['width']}Γ—{dims['height']}") ``` -#### Response -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "video_vertical_15s" - }, - "name": "15-Second Vertical Video", - "type": "video", - "duration": "15s", - "assets_required": [ - { - "asset_id": "video_file", - "asset_type": "video", - "asset_role": "hero_video", - "required": true, - "requirements": { - "duration": "15s", - "aspect_ratio": "9:16", - "resolution": "1080x1920", - "format": "MP4 H.264" - } - } - ] - }, - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "display_vertical_mobile" - }, - "name": "Vertical Mobile Banner", - "type": "display", - "dimensions": "320x480" - } - ] -} + + +### Filter by Type and Dimensions + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find video formats +const result = await testAgent.listCreativeFormats({ + type: 'video' +}); + +console.log(`Found ${result.formats.length} video formats`); + +// Group by duration +const byDuration = {}; +result.formats.forEach(format => { + const duration = format.requirements?.duration || 'unknown'; + if (!byDuration[duration]) byDuration[duration] = []; + byDuration[duration].push(format.name); +}); + +Object.entries(byDuration).forEach(([duration, formats]) => { + console.log(`${duration}s: ${formats.join(', ')}`); +}); ``` -### Example 4: Get Specs for Specific Format IDs +```python Python +from adcp import test_agent -"I got these format IDs from get_products - give me the full specs" +# Find video formats +result = test_agent.list_creative_formats( + type='video' +) -#### MCP Request -```json -{ - "tool": "list_creative_formats", - "arguments": { - "format_ids": [ - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_15s_hosted" - }, - { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_300x250" - } - ] - } -} +print(f"Found {len(result['formats'])} video formats") + +# Group by duration +from collections import defaultdict +by_duration = defaultdict(list) +for format in result['formats']: + duration = format.get('requirements', {}).get('duration', 'unknown') + by_duration[duration].append(format['name']) + +for duration, formats in by_duration.items(): + print(f"{duration}s: {', '.join(formats)}") ``` -#### Response -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "video_15s_hosted" - }, - "name": "15-Second Hosted Video", - "type": "video", - "duration": "15s", - "assets_required": [ - { - "asset_id": "video_file", - "asset_type": "video", - "asset_role": "hero_video", - "required": true, - "requirements": { - "duration": "15s", - "format": "MP4 H.264", - "resolution": ["1920x1080", "1280x720"], - "max_file_size_mb": 30 - } - } - ] - }, - { - "format_id": { - "agent_url": "https://sales-agent.example.com", - "id": "display_300x250" - }, - "name": "Medium Rectangle", - "type": "display", - "dimensions": "300x250", - "assets_required": [ - { - "asset_id": "banner_image", - "asset_type": "image", - "asset_role": "hero_image", - "required": true, - "width": 300, - "height": 250, - "acceptable_formats": ["jpg", "png", "gif"], - "max_file_size_kb": 200 - } - ] - } - ] -} + + +### Search by Name + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find mobile-optimized formats +const result = await testAgent.listCreativeFormats({ + name_search: 'mobile' +}); + +console.log(`Found ${result.formats.length} mobile formats`); + +result.formats.forEach(format => { + console.log(`- ${format.name} (${format.type})`); +}); ``` -### MCP Response +```python Python +from adcp import test_agent + +# Find mobile-optimized formats +result = test_agent.list_creative_formats( + name_search='mobile' +) + +print(f"Found {len(result['formats'])} mobile formats") -**Message:** +for format in result['formats']: + print(f"- {format['name']} ({format['type']})") ``` -I found 2 audio formats available. The standard 30-second format is recommended for maximum reach across all audio inventory. + + + +### Responsive Formats + + + +```javascript JavaScript +import { testAgent } from '@adcp/client/testing'; + +// Find formats that adapt to container size +const result = await testAgent.listCreativeFormats({ + is_responsive: true, + type: 'display' +}); + +console.log(`Found ${result.formats.length} responsive display formats`); + +result.formats.forEach(format => { + console.log(`${format.name}: Adapts to container`); +}); ``` -**Payload:** -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "audio_standard_30s" - }, - "name": "Standard Audio - 30 seconds", - "type": "audio", - "iab_specification": "DAAST 1.0", - "requirements": { - "duration": 30, - "file_types": ["mp3", "m4a"], - "bitrate_min": 128, - "bitrate_max": 320 - } - }, - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "display_carousel_5" - }, - "name": "Product Carousel - 5 Items", - "type": "display", - "assets_required": [ - { - "asset_type": "product_image", - "quantity": 5, - "requirements": { - "width": 300, - "height": 300, - "file_types": ["jpg", "png"], - "max_file_size": 150000 - } - }, - { - "asset_type": "logo", - "quantity": 1, - "requirements": { - "width": 200, - "height": 50, - "file_types": ["png", "svg"] - } - }, - { - "asset_type": "headline", - "quantity": 5, - "requirements": { - "max_length": 25, - "type": "text" - } - } - ] - } - ] -} +```python Python +from adcp import test_agent + +# Find formats that adapt to container size +result = test_agent.list_creative_formats( + is_responsive=True, + type='display' +) + +print(f"Found {len(result['formats'])} responsive display formats") + +for format in result['formats']: + print(f"{format['name']}: Adapts to container") ``` -### A2A Request + + +## Format Structure + +Each format includes: + +| Field | Description | +|-------|-------------| +| `format_id` | Structured identifier with agent_url and id | +| `name` | Human-readable format name | +| `type` | Format type (audio, video, display, dooh) | +| `requirements` | Technical requirements (duration, file types, bitrate, etc.) | +| `assets_required` | Array of required assets with specifications | +| `renders` | Array of rendered output pieces (dimensions, role) | + +### Asset Roles + +Common asset roles help identify asset purposes: + +- **`hero_image`** - Primary visual +- **`hero_video`** - Primary video content +- **`logo`** - Brand logo +- **`headline`** - Primary text +- **`body_text`** - Secondary text +- **`call_to_action`** - CTA button text +- **`third_party_tag`** - External ad tag + +## Asset Types Filter Logic + +The `asset_types` parameter uses **OR logic** - formats matching ANY specified asset type are returned. + +**Example**: `asset_types: ['html', 'javascript', 'image']` +- Returns formats accepting html OR javascript OR image +- Use case: "Show me formats I can use with any of my available asset types" + +**To find formats requiring specific combinations**, filter results after retrieval: -#### Natural Language Invocation ```javascript -await a2a.send({ - message: { - parts: [{ - kind: "text", - text: "Show me all your supported creative formats" - }] - } +// Find formats requiring BOTH image AND text +const result = await agent.listCreativeFormats(); +const imageAndText = result.formats.filter(format => { + const assetTypes = format.assets_required.map(a => a.asset_type); + return assetTypes.includes('image') && assetTypes.includes('text'); }); ``` -#### Explicit Skill Invocation +## Dimension Filtering for Multi-Render Formats + +Some formats produce multiple rendered pieces: +- **Video with companion banner** - Primary video (1920Γ—1080) + banner (300Γ—250) +- **Adaptive displays** - Desktop (728Γ—90) + mobile (320Γ—50) +- **DOOH installations** - Multiple screens with different dimensions + +Dimension filters match if **at least one render** fits: + ```javascript -await a2a.send({ - message: { - parts: [{ - kind: "data", - data: { - skill: "list_creative_formats", - parameters: { - standard_only: false - } - } - }] - } +// Find formats with ANY render ≀ 300Γ—250 +const result = await agent.listCreativeFormats({ + max_width: 300, + max_height: 250 }); + +// Returns formats where at least one render fits 300Γ—250 slot +// May also include larger companion pieces ``` -### A2A Response +## Error Handling -```json -{ - "artifacts": [{ - "name": "creative_formats", - "parts": [ - { - "kind": "text", - "text": "We support 47 creative formats across video, audio, and display. Video formats dominate with 23 options including standard pre-roll and innovative interactive formats. For maximum compatibility, I recommend using IAB standard formats which are accepted by 95% of our inventory." - }, - { - "kind": "data", - "data": { - "formats": [ - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard_30s" - }, - "name": "Standard Video - 30 seconds", - "type": "video", - "iab_specification": "VAST 4.2", - "requirements": { - "duration": 30, - "width": 1920, - "height": 1080, - "file_types": ["mp4", "webm"], - "max_file_size": 50000000, - "min_bitrate": 2500, - "max_bitrate": 8000 - } - } - // ... 46 more formats - ] - } - } - ] - }] -} -``` +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `FORMAT_NOT_FOUND` | Requested format_id doesn't exist | Verify format_id from get_products response | +| `INVALID_REQUEST` | Invalid filter parameters | Check parameter types and values | +| `AGENT_NOT_FOUND` | Referenced creative agent unavailable | Format may be from deprecated agent | -## Scenarios +## Best Practices -### Discovering Standard Video Formats +**1. Use format_ids Parameter** +Most efficient way to get specs for formats returned by `get_products`. -**Request:** -```json -{ - "type": "video", - "standard_only": true -} -``` +**2. Cache Format Specifications** +Format specs rarely change - cache by format_id to reduce API calls. -**Message:** -``` -Found 8 standard video formats following IAB VAST specifications. The 30-second and 15-second pre-roll formats have the broadest inventory coverage. -``` +**3. Filter by Asset Types for Third-Party Tags** +Search for `asset_types: ['html']` or `['javascript']` to find tag-accepting formats. -**Payload:** -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard_30s" - }, - "name": "Standard Video - 30 seconds", - "type": "video", - "iab_specification": "VAST 4.2", - "requirements": { - "duration": 30, - "width": 1920, - "height": 1080, - "file_types": ["mp4", "webm"], - "max_file_size": 50000000 - } - }, - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard_15s" - }, - "name": "Standard Video - 15 seconds", - "type": "video", - "iab_specification": "VAST 4.2", - "requirements": { - "duration": 15, - "width": 1920, - "height": 1080, - "file_types": ["mp4", "webm"], - "max_file_size": 25000000 - } - } - // ... 6 more standard video formats - ] -} -``` +**4. Consider Multi-Render Formats** +Check `renders` array length - some formats produce multiple pieces requiring multiple placements. -### Finding Display Carousel Formats +**5. Validate Asset Requirements** +Ensure your creative assets match format specifications before building creatives. -**Request:** -```json -{ - "type": "display" -} -``` +## Next Steps -**Message:** -``` -I found 15 display formats including standard IAB sizes and innovative formats like product carousels. Standard sizes (300x250, 728x90) have the broadest reach, while carousel formats offer higher engagement for e-commerce campaigns. -``` +After discovering formats: -**Payload:** -```json -{ - "formats": [ - { - "format_id": { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "display_carousel_5" - }, - "name": "Product Carousel - 5 Items", - "type": "display", - "assets_required": [ - { - "asset_type": "product_image", - "quantity": 5, - "requirements": { - "width": 300, - "height": 300, - "file_types": ["jpg", "png"] - } - } - ] - } - // ... additional display formats - ] -} -``` +1. **Build Creatives**: Use [`build_creative`](/docs/creative/task-reference/build_creative) to assemble assets into format +2. **Preview**: Use [`preview_creative`](/docs/creative/task-reference/preview_creative) to see visual output +3. **Validate**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) with `dry_run: true` +4. **Upload**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) to upload to media buy + +## Learn More -## Usage Notes - -- **Primary use case**: Get creative specifications after `get_products` returns format IDs -- **Format IDs are just strings** until you get their specifications from this tool -- **Standard formats** follow IAB specifications and work across multiple publishers -- **Custom formats** (like "homepage_takeover") are publisher-specific with unique requirements -- **The `format_ids` parameter** is the most efficient way to get specs for specific formats returned by products -- **Asset requirements vary by format type**: - - Audio formats: duration, file types, bitrate specifications - - Video formats: resolution, aspect ratio, codec, delivery method - - Display formats: dimensions, file types, file size limits - - Rich media formats: multiple assets with specific roles and requirements - -## Implementation Guide - -### Generating Format Messages - -The `message` field should provide helpful context about available formats: - -```python -def generate_formats_message(formats, filter_type=None): - total_count = len(formats) - standard_count = sum(1 for f in formats if f.is_standard) - - # Analyze format distribution - by_type = {} - for format in formats: - by_type[format.type] = by_type.get(format.type, 0) + 1 - - # Generate insights - if filter_type: - recommendations = get_format_recommendations(formats, filter_type) - return f"I found {total_count} {filter_type} formats available. {recommendations}" - else: - type_summary = format_type_distribution(by_type) - compatibility_note = f"For maximum compatibility, I recommend using IAB standard formats which are accepted by {calculate_standard_coverage()}% of our inventory." - return f"We support {total_count} creative formats across {', '.join(by_type.keys())}. {type_summary} {compatibility_note}" - -def get_format_recommendations(formats, format_type): - if format_type == "video": - return "The standard 30-second format provides the broadest reach, while 15-second formats work best for social platforms. Consider creating multiple durations to maximize inventory access." - elif format_type == "audio": - return "The standard 30-second format is recommended for maximum reach across all audio inventory. 15-second spots are ideal for podcasts and streaming audio." - elif format_type == "display": - return "Standard IAB sizes (300x250, 728x90) have the most inventory. Rich media formats like carousels drive higher engagement but have limited availability." -``` \ No newline at end of file +- [Format Schema](https://adcontextprotocol.org/schemas/v1/core/format.json) - Complete format structure +- [Asset Types](/docs/creative/asset-types) - Asset specification details +- [Standard Formats](/docs/media-buy/capability-discovery/implementing-standard-formats) - IAB-compatible reference formats diff --git a/docs/media-buy/task-reference/sync_creatives.mdx b/docs/media-buy/task-reference/sync_creatives.mdx index d11695d6..a13dc0a8 100644 --- a/docs/media-buy/task-reference/sync_creatives.mdx +++ b/docs/media-buy/task-reference/sync_creatives.mdx @@ -1,567 +1,710 @@ --- title: sync_creatives +sidebar_position: 4 --- # sync_creatives -Synchronize creative assets with the centralized creative library using upsert semantics. This task supports bulk operations, partial updates, assignment management, and comprehensive validation for efficient creative library management. +Upload and manage creative assets for media buys. Supports bulk uploads, upsert semantics, and third-party tags. -**Response Time**: Instant to days (returns `completed`, `working` < 120s, or `submitted` for hours/days) +**Response Time**: Instant to minutes (depends on file processing and validation) -## Overview +**Request Schema**: [`/schemas/v1/media-buy/sync-creatives-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/sync-creatives-request.json) +**Response Schema**: [`/schemas/v1/media-buy/sync-creatives-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/sync-creatives-response.json) -The `sync_creatives` task provides a powerful, efficient approach to creative library management using **upsert semantics** - creatives are either created (if they don't exist) or updated (if they do exist) based on their `creative_id`. This eliminates the need to check existence before uploading and enables seamless bulk operations. +## Quick Start -**Key Features:** -- **Bulk Operations**: Process up to 100 creatives per request -- **Upsert Semantics**: Automatic create-or-update behavior -- **Patch Mode**: Update only specified fields while preserving others -- **Assignment Management**: Bulk assign creatives to packages in the same request -- **Validation Modes**: Choose between strict (fail-fast) or lenient (process-valid) validation -- **Dry Run**: Preview changes before applying them -- **Generative Creatives**: Submit brand manifest and generation prompts for AI or human-created creatives -- **Comprehensive Reporting**: Detailed results for each creative processed +Upload creatives with package assignments: -## Request Parameters - -### Core Parameters - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `creatives` | array | Yes | Array of creative assets to sync (max 100) | -| `patch` | boolean | No | Partial update mode (default: false) | -| `dry_run` | boolean | No | Preview changes without applying (default: false) | -| `validation_mode` | enum | No | Validation strictness: "strict" or "lenient" (default: "strict") | -| `push_notification_config` | PushNotificationConfig | No | Optional webhook for async sync notifications (see Webhook Configuration below) | - -### Assignment Management - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `assignments` | object | No | Bulk creative-to-package assignments (simple package-level only - see note below) | -| `delete_missing` | boolean | No | Archive creatives not in this sync (default: false) | + -**Note on Placement Targeting:** The `assignments` field only supports package-level assignments without placement targeting. To assign creatives to specific placements within a product, use [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) or [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) with `creative_assignments[].placement_ids`. +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -### Creative Asset Structure - -Each creative in the `creatives` array follows the [Creative Asset schema](https://adcontextprotocol.org/schemas/v1/core/creative-asset.json) with support for: - -**Hosted Assets:** -- `creative_id`, `name`, `format_id` (required) -- `assets` object with asset types like `image`, `video`, `audio` (required) -- `tags` (optional) - -**Third-Party Served Assets:** -- `creative_id`, `name`, `format_id` (required) -- `assets` object with `vast`, `daast`, `html`, or `javascript` asset types (required) -- `tags` (optional) - -**Generative Creatives:** -- `creative_id`, `name`, `format_id` (required - references a generative format) -- `assets` object with `promoted_offerings` and `generation_prompt` -- Publisher creates the creative (AI or human - buyer may not know the implementation method) -- Response includes `preview_url` when ready for review -- Note: Some buyers may care about creation method for brand safety or compliance reasons - -## Webhook Configuration (Task-Specific) +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); + +const agent = client.agent('test-agent'); +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + creatives: [{ + creative_id: 'creative_video_001', + name: 'Summer Sale 30s', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'video_standard_30s' + }, + assets: [{ + asset_type: 'video', + url: 'https://cdn.example.com/summer-sale-30s.mp4' + }], + package_assignments: [{ + package_id: 'pkg_67890', + status: 'active' + }] + }] +}); + +console.log(`Synced ${result.creatives.length} creatives`); +``` -For large bulk operations or creative approval workflows, you can provide a task-specific webhook to be notified when the sync completes: +```python Python +from adcp import ADCPMultiAgentClient -```json -{ - "creatives": [/* up to 100 creatives */], - "push_notification_config": { - "url": "https://buyer.com/webhooks/creative-sync", - "authentication": { - "schemes": ["HMAC-SHA256"], - "credentials": "shared_secret_32_chars" +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - } -} +}]) + +agent = client.agent('test-agent') +result = agent.sync_creatives( + media_buy_id='mb_12345', + creatives=[{ + 'creative_id': 'creative_video_001', + 'name': 'Summer Sale 30s', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'video_standard_30s' + }, + 'assets': [{ + 'asset_type': 'video', + 'url': 'https://cdn.example.com/summer-sale-30s.mp4' + }], + 'package_assignments': [{ + 'package_id': 'pkg_67890', + 'status': 'active' + }] + }] +) + +print(f"Synced {len(result['creatives'])} creatives") ``` -**When webhooks are sent:** -- Bulk sync takes longer than ~120 seconds (status: `working` β†’ `completed`) -- Creative approval required (status: `submitted` β†’ `completed`) -- Large creative uploads processing asynchronously + -**Webhook payload:** -- Protocol fields at top-level (operation_id, task_type, status, etc.) -- `result` contains complete sync_creatives response with summary and results +## Request Parameters -See [Webhook Security](/docs/protocols/core-concepts.mdx#security) for authentication details. +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `media_buy_id` | string | Yes | Media buy to sync creatives for | +| `creatives` | Creative[] | Yes | Creative assets to upload/update | +| `mode` | string | No | Sync mode: `"upsert"` (default), `"dry_run"`, `"patch"` | -## Response Format +### Creative Object -The response provides comprehensive details about the sync operation: +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `creative_id` | string | Yes | Unique identifier for this creative | +| `name` | string | Yes | Human-readable name | +| `format_id` | FormatId | Yes | Format specification (structured object with `agent_url` and `id`) | +| `assets` | Asset[] | Yes* | Asset files (URLs or inline). *Not required for third-party tags | +| `third_party_tag` | string | No | Third-party ad tag (HTML/JavaScript) | +| `package_assignments` | Assignment[] | No | Package assignments with status | +| `brand_safe` | boolean | No | Brand safety certification flag | -```json -{ - "message": "Sync completed: 3 created, 2 updated, 1 unchanged", - "context_id": "ctx_sync_123456", - "status": "completed", - "dry_run": false, - "creatives": [ - { - "creative_id": "hero_video_30s", - "action": "created", - "platform_id": "plt_123456" - }, - { - "creative_id": "banner_300x250", - "action": "updated", - "platform_id": "plt_789012", - "changes": ["click_url"] - }, - { - "creative_id": "native_ad_01", - "action": "unchanged", - "platform_id": "plt_345678" - } - ] -} -``` +### Asset Object -## Generative Creative Workflow +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `asset_type` | string | Yes | Type: `"video"`, `"image"`, `"html"`, `"javascript"`, etc. | +| `url` | string | Yes* | Public CDN URL to asset file. *Not required if using inline content | +| `content` | string | No | Inline asset content (base64 for binary, plain text for HTML/JS) | -For generative formats, buyers submit a creative manifest (brand information + generation instructions) rather than finished assets. The publisher then creates the creative - either through AI generation, human designers, or a hybrid approach. +### Package Assignment Object -**Key Characteristics:** +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `package_id` | string | Yes | Package to assign creative to | +| `status` | string | No | Assignment status: `"active"` (default), `"paused"` | -1. **Implementation Agnostic**: Buyer submits the same request whether the publisher uses AI, human designers, or both -2. **Async by Default**: Response time ranges from seconds (AI) to days (human review) -3. **Preview-First**: Publisher returns `preview_url` for buyer approval before campaign launch -4. **Simple Approval**: Set `approved: true` to finalize or `approved: false` with updated `generation_prompt` to request changes -5. **Conversational Refinement**: Use `context_id` from response to continue the conversation +## Response -**Note on Transparency:** Some buyers may care about creation method (AI vs human) for brand safety, compliance, or quality reasons. Publishers should communicate their approach during format discovery or setup. +| Field | Description | +|-------|-------------| +| `creatives` | Array of synced creative objects with `platform_creative_id` populated | +| `errors` | Array of validation/processing errors (if any) | +| `warnings` | Array of non-blocking warnings (if any) | -**Protocol Context**: The `context_id` is managed at the protocol level (automatic in A2A, manual in MCP) and is not part of the task request parameters. See [Context Management](/docs/protocols/context-management) for details. +**Each creative in response includes**: +- All request fields +- `platform_creative_id` - Platform's internal ID +- Processing status and timestamps -**Workflow Steps:** +**See schema for complete field list**: [sync-creatives-response.json](https://adcontextprotocol.org/schemas/v1/media-buy/sync-creatives-response.json) -``` -1. Buyer submits creative with promoted_offerings + generation_prompt -2. Publisher responds with status: "submitted" or "working" -3. Publisher creates creative (AI/human/hybrid) -4. Publisher responds with status: "completed" + preview_url -5. Buyer reviews preview: - - Approve: Re-submit with approved: true - - Refine: Re-submit with approved: false + updated generation_prompt -``` +## Common Scenarios + +### Bulk Upload with Assignments -**Example Generative Formats:** -- `premium_bespoke_display` - Custom-designed display ad (human designer, 24-48h) -- `ai_native_responsive` - AI-generated native ad (automated, under 60s) -- `hybrid_video_30s` - AI draft + human polish (hybrid, 2-4h) +Upload multiple creatives and assign them to packages in one call: -The format definition specifies expected turnaround time, but the buyer's workflow is identical regardless of implementation. + -## Usage Examples +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -### Example 1: Full Sync with New Creatives +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); -Upload new creatives with automatic assignment: +const agent = client.agent('test-agent'); -```json -{ - "creatives": [ +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + creatives: [ { - "creative_id": "hero_video_30s", - "name": "Brand Hero Video 30s", - "format_id": { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "video_30s_vast" - }, - "assets": { - "vast_tag": { - "asset_type": "vast", - "delivery_type": "url", - "url": "https://vast.example.com/video/123", - "vast_version": "4.1", - "duration_ms": 30000 - } + creative_id: 'creative_display_001', + name: 'Summer Sale Banner 300x250', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'display_300x250' }, - "tags": ["q1_2024", "video", "hero"] + assets: [{ + asset_type: 'image', + url: 'https://cdn.example.com/banner-300x250.jpg' + }], + package_assignments: [ + { package_id: 'pkg_premium', status: 'active' }, + { package_id: 'pkg_standard', status: 'active' } + ], + brand_safe: true }, { - "creative_id": "banner_300x250", - "name": "Standard Banner", - "format_id": { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_300x250" + creative_id: 'creative_video_002', + name: 'Product Demo 15s', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'video_standard_15s' }, - "assets": { - "banner_image": { - "asset_type": "image", - "url": "https://cdn.example.com/banner.jpg", - "width": 300, - "height": 250, - "format": "jpg" - } + assets: [{ + asset_type: 'video', + url: 'https://cdn.example.com/demo-15s.mp4' + }], + package_assignments: [ + { package_id: 'pkg_premium', status: 'active' } + ], + brand_safe: true + }, + { + creative_id: 'creative_display_002', + name: 'Summer Sale Banner 728x90', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'display_728x90' }, - "tags": ["q1_2024", "display"] + assets: [{ + asset_type: 'image', + url: 'https://cdn.example.com/banner-728x90.jpg' + }], + package_assignments: [ + { package_id: 'pkg_standard', status: 'active' } + ], + brand_safe: true } - ], - "assignments": { - "hero_video_30s": ["pkg_ctv_001", "pkg_ctv_002"], - "banner_300x250": ["pkg_display_001"] - } -} -``` + ] +}); -### Example 2: Patch Update - Asset URLs Only +console.log(`Successfully synced ${result.creatives.length} creatives`); +result.creatives.forEach(creative => { + console.log(` ${creative.name}: ${creative.platform_creative_id}`); +}); +``` -Update asset URLs without affecting other creative properties: +```python Python +from adcp import ADCPMultiAgentClient -```json -{ - "creatives": [ - { - "creative_id": "hero_video_30s", - "assets": { - "vast_tag": { - "asset_type": "vast", - "delivery_type": "url", - "url": "https://vast.example.com/video/new-campaign" - } - } - }, - { - "creative_id": "banner_300x250", - "assets": { - "banner_image": { - "asset_type": "image", - "url": "https://cdn.example.com/new-banner.jpg" - } - } +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - ], - "patch": true -} +}]) + +agent = client.agent('test-agent') + +result = agent.sync_creatives( + media_buy_id='mb_12345', + creatives=[ + { + 'creative_id': 'creative_display_001', + 'name': 'Summer Sale Banner 300x250', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'display_300x250' + }, + 'assets': [{ + 'asset_type': 'image', + 'url': 'https://cdn.example.com/banner-300x250.jpg' + }], + 'package_assignments': [ + {'package_id': 'pkg_premium', 'status': 'active'}, + {'package_id': 'pkg_standard', 'status': 'active'} + ], + 'brand_safe': True + }, + { + 'creative_id': 'creative_video_002', + 'name': 'Product Demo 15s', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'video_standard_15s' + }, + 'assets': [{ + 'asset_type': 'video', + 'url': 'https://cdn.example.com/demo-15s.mp4' + }], + 'package_assignments': [ + {'package_id': 'pkg_premium', 'status': 'active'} + ], + 'brand_safe': True + }, + { + 'creative_id': 'creative_display_002', + 'name': 'Summer Sale Banner 728x90', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'display_728x90' + }, + 'assets': [{ + 'asset_type': 'image', + 'url': 'https://cdn.example.com/banner-728x90.jpg' + }], + 'package_assignments': [ + {'package_id': 'pkg_standard', 'status': 'active'} + ], + 'brand_safe': True + } + ] +) + +print(f"Successfully synced {len(result['creatives'])} creatives") +for creative in result['creatives']: + print(f" {creative['name']}: {creative['platform_creative_id']}") ``` -### Example 3: Third-Party HTML Tag Upload + -Upload a creative with third-party HTML tag: +### Patch Update (Change Assignments Only) -```json -{ - "creatives": [ - { - "creative_id": "programmatic_display", - "name": "Programmatic Display Ad", - "format_id": { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_300x250" - }, - "assets": { - "ad_tag": { - "asset_type": "html", - "content": "
", - "width": 300, - "height": 250 - } - }, - "tags": ["programmatic", "display"] - } - ] -} -``` +Update package assignments without re-uploading assets: -### Example 4: Generative Creative Submission + -Submit a creative manifest for publisher to create (AI or human): +```javascript JavaScript +import { ADCPMultiAgentClient} from '@adcp/client'; -**Request:** -```json -{ - "creatives": [ - { - "creative_id": "holiday_hero_display", - "name": "Holiday Campaign Hero Display", - "format_id": { - "agent_url": "https://publisher.com/.well-known/adcp/sales", - "id": "premium_bespoke_display" - }, - "assets": { - "promoted_offerings": { - "asset_type": "promoted_offerings", - "url": "https://retailer.com", - "colors": { - "primary": "#C41E3A", - "secondary": "#165B33", - "accent": "#FFD700" - }, - "tone": "Warm, festive, family-oriented" - }, - "generation_prompt": { - "asset_type": "text", - "content": "Create a holiday campaign featuring our winter collection. Emphasize warmth, family togetherness, and quality. Include subtle holiday elements without being overtly religious." - } - }, - "tags": ["holiday", "q4_2024", "hero"] - } - ] -} +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); + +const agent = client.agent('test-agent'); + +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + mode: 'patch', + creatives: [{ + creative_id: 'creative_video_001', + name: 'Summer Sale 30s', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'video_standard_30s' + }, + package_assignments: [ + { package_id: 'pkg_premium', status: 'active' }, + { package_id: 'pkg_standard', status: 'paused' } + ] + }] +}); + +console.log('Updated assignments for creative:', result.creatives[0].creative_id); ``` -**Note:** The `url` field in the `promoted_offerings` asset represents the advertiser's brand or product website (e.g., `https://retailer.com`), not a reference to a cached manifest. Publishers can use this URL to gather additional brand context if needed. +```python Python +from adcp import ADCPMultiAgentClient -**Initial Response (Async):** -```json -{ - "status": "submitted", - "message": "Creative submitted for creation", - "context_id": "ctx_abc123", - "creatives": [ - { - "creative_id": "holiday_hero_display", - "action": "created", - "platform_id": "pub_creative_789" +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - ] -} +}]) + +agent = client.agent('test-agent') + +result = agent.sync_creatives( + media_buy_id='mb_12345', + mode='patch', + creatives=[{ + 'creative_id': 'creative_video_001', + 'name': 'Summer Sale 30s', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'video_standard_30s' + }, + 'package_assignments': [ + {'package_id': 'pkg_premium', 'status': 'active'}, + {'package_id': 'pkg_standard', 'status': 'paused'} + ] + }] +) + +print(f"Updated assignments for creative: {result['creatives'][0]['creative_id']}") ``` -**Later Response (When Ready):** -```json -{ - "status": "completed", - "message": "Creative ready for review", - "context_id": "ctx_abc123", - "creatives": [ - { - "creative_id": "holiday_hero_display", - "action": "created", - "platform_id": "pub_creative_789", - "preview_url": "https://publisher.com/preview/pub_creative_789", - "expires_at": "2024-12-20T00:00:00Z" - } - ] -} -``` + -**Approval (Buyer likes it):** -```json -{ - "creatives": [ - { - "creative_id": "holiday_hero_display", - "approved": true - } - ] -} -``` +### Third-Party Ad Tags -**Or Request Changes (Buyer wants refinement):** -```json -{ - "creatives": [ - { - "creative_id": "holiday_hero_display", - "approved": false, - "assets": { - "generation_prompt": { - "asset_type": "text", - "content": "Make the colors more vibrant and emphasize the sale pricing more prominently" - } - } - } - ] -} -``` +Use third-party ad serving with HTML/JavaScript tags: -_Note: Conversational context is maintained automatically by the protocol layer - no explicit `context_id` parameter is needed in requests._ + -The buyer may not know if this creative was generated by AI in 20 seconds or designed by a human team over 2 days. The workflow is identical either way, though publishers should communicate their approach for buyers who care about creation method. +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -### Example 5: Dry Run Preview +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); + +const agent = client.agent('test-agent'); + +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + creatives: [{ + creative_id: 'creative_3p_001', + name: 'DCM Tag - Summer Campaign', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'display_300x250' + }, + third_party_tag: ` + + + + + `, + package_assignments: [{ + package_id: 'pkg_standard', + status: 'active' + }] + }] +}); + +console.log('Third-party tag uploaded:', result.creatives[0].creative_id); +``` -Preview changes before applying them: +```python Python +from adcp import ADCPMultiAgentClient -```json -{ - "creatives": [ - { - "creative_id": "test_banner", - "name": "Test Banner Creative", - "format_id": { - "agent_url": "https://creatives.adcontextprotocol.org", - "id": "display_300x250" - }, - "assets": { - "banner_image": { - "asset_type": "image", - "url": "https://cdn.example.com/test-banner.jpg", - "width": 300, - "height": 250 - } - } +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - ], - "dry_run": true, - "validation_mode": "lenient" -} +}]) + +agent = client.agent('test-agent') + +result = agent.sync_creatives( + media_buy_id='mb_12345', + creatives=[{ + 'creative_id': 'creative_3p_001', + 'name': 'DCM Tag - Summer Campaign', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'display_300x250' + }, + 'third_party_tag': ''' + + + + + ''', + 'package_assignments': [{ + 'package_id': 'pkg_standard', + 'status': 'active' + }] + }] +) + +print(f"Third-party tag uploaded: {result['creatives'][0]['creative_id']}") ``` -### Example 6: Library Replacement (Advanced) + -Replace entire creative library with new set (use with extreme caution): +### Generative Creatives -```json -{ - "creatives": [ - // ... all creatives that should exist in the library - ], - "delete_missing": true, - "validation_mode": "strict" -} -``` +Use the creative agent to generate creatives from a brand manifest. See the [Generative Creatives guide](/docs/creative/generative-creative) for complete workflow details. -## Operational Modes + -### Patch vs Full Upsert +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -**Full Upsert Mode (default):** -- Replaces the entire creative with provided data -- Missing optional fields are cleared/reset to defaults -- Use when you want to ensure complete creative definition +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); + +const agent = client.agent('test-agent'); + +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + creatives: [{ + creative_id: 'creative_gen_001', + name: 'AI-Generated Summer Banner', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'display_300x250' + }, + assets: [{ + asset_type: 'manifest', + url: 'https://cdn.example.com/brand-manifest.json' + }], + package_assignments: [{ + package_id: 'pkg_standard', + status: 'active' + }] + }] +}); + +console.log('Generative creative synced:', result.creatives[0].creative_id); +``` -**Patch Mode (`patch: true`):** -- Updates only the fields specified in the request -- Preserves existing values for unspecified fields -- Use for selective updates (e.g., updating click URLs only) +```python Python +from adcp import ADCPMultiAgentClient -### Validation Modes +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]) + +agent = client.agent('test-agent') + +result = agent.sync_creatives( + media_buy_id='mb_12345', + creatives=[{ + 'creative_id': 'creative_gen_001', + 'name': 'AI-Generated Summer Banner', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'display_300x250' + }, + 'assets': [{ + 'asset_type': 'manifest', + 'url': 'https://cdn.example.com/brand-manifest.json' + }], + 'package_assignments': [{ + 'package_id': 'pkg_standard', + 'status': 'active' + }] + }] +) + +print(f"Generative creative synced: {result['creatives'][0]['creative_id']}") +``` -**Strict Mode (default):** -- Entire sync operation fails if any creative has validation errors -- Ensures data consistency and integrity -- Use for production uploads where quality is critical + -**Lenient Mode:** -- Processes valid creatives and reports errors for invalid ones -- Allows partial success in bulk operations -- Use for large imports where some failures are acceptable +### Dry Run Validation -## Error Handling +Validate creative configuration without uploading: -### Validation Errors + -Common validation scenarios and their handling: +```javascript JavaScript +import { ADCPMultiAgentClient } from '@adcp/client'; -```json -{ - "results": [ - { - "creative_id": "invalid_creative", - "action": "failed", - "errors": [ - "Missing required field: format_id", - "Missing required field: assets", - "Asset 'vast_tag' has invalid asset_type: must be one of [image, video, audio, text, html, css, javascript, vast, daast, promoted_offerings, url]" - ] - } - ] +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + } +}]); + +const agent = client.agent('test-agent'); + +const result = await agent.syncCreatives({ + media_buy_id: 'mb_12345', + mode: 'dry_run', + creatives: [{ + creative_id: 'creative_test_001', + name: 'Test Creative', + format_id: { + agent_url: 'https://creatives.adcontextprotocol.org', + id: 'video_standard_30s' + }, + assets: [{ + asset_type: 'video', + url: 'https://cdn.example.com/test-video.mp4' + }], + package_assignments: [{ + package_id: 'pkg_unknown', + status: 'active' + }] + }] +}); + +if (result.errors && result.errors.length > 0) { + console.log('Validation errors found:'); + result.errors.forEach(error => console.log(` - ${error.message}`)); +} else { + console.log('Validation passed! Ready to sync.'); } ``` -### Assignment Errors - -When assignments fail, they're reported within each creative's result: +```python Python +from adcp import ADCPMultiAgentClient -```json -{ - "creatives": [ - { - "creative_id": "hero_video_30s", - "action": "updated", - "assigned_to": ["pkg_ctv_001"], - "assignment_errors": { - "pkg_invalid_123": "Package not found or access denied" - } +client = ADCPMultiAgentClient([{ + 'id': 'test-agent', + 'agent_uri': 'https://test-agent.adcontextprotocol.org/mcp', + 'protocol': 'mcp', + 'auth': { + 'type': 'bearer', + 'token': '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } - ] -} +}]) + +agent = client.agent('test-agent') + +result = agent.sync_creatives( + media_buy_id='mb_12345', + mode='dry_run', + creatives=[{ + 'creative_id': 'creative_test_001', + 'name': 'Test Creative', + 'format_id': { + 'agent_url': 'https://creatives.adcontextprotocol.org', + 'id': 'video_standard_30s' + }, + 'assets': [{ + 'asset_type': 'video', + 'url': 'https://cdn.example.com/test-video.mp4' + }], + 'package_assignments': [{ + 'package_id': 'pkg_unknown', + 'status': 'active' + }] + }] +) + +if result.get('errors'): + print('Validation errors found:') + for error in result['errors']: + print(f" - {error['message']}") +else: + print('Validation passed! Ready to sync.') ``` -## Best Practices - -### 1. Batch Size Management -- Stay within 100 creatives per request limit -- For large libraries, break into multiple sync requests -- Consider rate limiting to avoid overwhelming the platform + -### 2. Validation Strategy -- Use `dry_run: true` to preview changes for large updates -- Start with `validation_mode: "strict"` to catch data quality issues -- Switch to `lenient` mode for large imports with expected failures +## Sync Modes -### 3. Creative ID Strategy -- Use consistent, meaningful creative ID conventions -- Include format hints in IDs (e.g., `hero_video_30s`, `banner_300x250`) -- Avoid special characters that might cause URL encoding issues +### Upsert (default) +- Creates new creatives or updates existing by `creative_id` +- Merges package assignments (additive) +- Full asset replacement if assets provided +- Use when initially uploading or doing complete updates -### 4. Assignment Management -- Group related package assignments in single requests -- Use assignment bulk operations for efficiency -- Monitor assignment results for failed package assignments +### Patch +- Updates only specified fields +- Additive package assignments (doesn't remove existing) +- Does not replace assets unless explicitly provided +- Use when modifying assignments without re-uploading assets -### 5. Error Recovery -- Implement retry logic for transient failures -- Parse detailed error responses to identify data quality issues -- Use patch mode for correcting specific field errors +### Dry Run +- Validates request without making changes +- Returns errors and warnings +- Does not process assets or create creatives +- Use for pre-flight validation checks -## Migration from Legacy Creative Tasks - -The `sync_creatives` task replaces previous action-based creative management approaches: +## Error Handling -**Old Approach:** -```json -{ - "action": "upload", - "creatives": [...] -} -``` +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `INVALID_FORMAT` | Format not supported by product | Check product's supported formats via `list_creative_formats` | +| `ASSET_PROCESSING_FAILED` | Asset file corrupt or invalid | Verify asset meets format requirements (codec, dimensions, duration) | +| `PACKAGE_NOT_FOUND` | Package ID doesn't exist in media buy | Verify `package_id` from `create_media_buy` response | +| `THIRD_PARTY_TAG_INVALID` | 3P tag failed validation | Check tag syntax and required macros (`${CLICK_URL}`, `${CACHEBUSTER}`) | +| `BRAND_SAFETY_VIOLATION` | Creative failed brand safety scan | Review content against publisher's brand safety guidelines | +| `FORMAT_MISMATCH` | Assets don't match format requirements | Verify asset types and specifications match format definition | +| `DUPLICATE_CREATIVE_ID` | Creative ID already exists in different media buy | Use unique `creative_id` or sync to correct media buy | -**New Approach:** -```json -{ - "creatives": [...] // Automatic upsert behavior -} -``` +## Best Practices -**Key Changes:** -- No `action` parameter needed - upsert behavior is automatic -- Bulk operations are the default, not an add-on -- Assignment management integrated into sync operation -- More granular control with patch mode and validation modes +1. **Use upsert semantics** - Same `creative_id` updates existing creative rather than creating duplicates. This allows iterative creative development. -## Platform Considerations +2. **Validate first** - Use `mode: "dry_run"` to catch errors before actual upload. This saves bandwidth and processing time. -### Google Ad Manager -- Requires policy compliance review for new creatives -- Supports standard IAB formats with automatic format validation -- Creative approval typically within 24 hours +3. **Batch assignments** - Include all package assignments in single sync call to avoid race conditions between updates. -### Kevel -- Supports custom creative formats and templates -- Real-time creative decisioning capabilities -- Flexible template-based native ad support +4. **CDN-hosted assets** - Use publicly accessible CDN URLs for faster processing. Platforms can fetch assets directly without proxy delays. -### Triton Digital -- Audio-specific platform with specialized format requirements -- Station-level creative targeting capabilities -- Supports DAAST and standard audio ad formats +5. **Third-party tags** - Include all required macros: + - `${CLICK_URL}` - Click tracking wrapper + - `${CACHEBUSTER}` - Cache-busting random number + - Platform-specific macros as documented -## Related Tasks +6. **Brand manifests** - For generative creatives, validate manifest schema before syncing to avoid processing failures. -- [`list_creatives`](/docs/media-buy/task-reference/list_creatives) - Query creative library with filtering and search -- [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) - Create campaigns that use library creatives -- [`list_creative_formats`](/docs/media-buy/task-reference/list_creative_formats) - Discover supported creative formats +7. **Check format support** - Use `list_creative_formats` to verify product supports your creative formats before uploading. ---- +## Next Steps -*The sync_creatives task provides the foundation for efficient creative library management in AdCP, enabling bulk operations and flexible update patterns while maintaining data quality and consistency.* \ No newline at end of file +- [list_creative_formats](/docs/media-buy/task-reference/list_creative_formats) - Check supported formats before upload +- [Generative Creatives Guide](/docs/creative/generative-creative) - Generate creatives from brand manifests +- [get_media_buy_delivery](/docs/media-buy/task-reference/get_media_buy_delivery) - Monitor creative performance +- [Creative Asset Types](/docs/creative/asset-types) - Technical requirements for assets diff --git a/docs/media-buy/task-reference/update_media_buy.mdx b/docs/media-buy/task-reference/update_media_buy.mdx index 33ecfcc0..9ad4d1a9 100644 --- a/docs/media-buy/task-reference/update_media_buy.mdx +++ b/docs/media-buy/task-reference/update_media_buy.mdx @@ -1,525 +1,274 @@ --- title: update_media_buy -sidebar_position: 7 +sidebar_position: 8 --- # update_media_buy -Update campaign and package settings. This task supports partial updates and handles any required approvals. +Modify an existing media buy using PATCH semantics. Supports campaign-level and package-level updates. -**Response Time**: Instant to days (returns `completed`, `working` < 120s, or `submitted` for hours/days) +**Response Time**: Instant to days (status: `completed`, `working` < 120s, or `submitted` for manual review) +**PATCH Semantics**: Only specified fields are updated; omitted fields remain unchanged. -**Request Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-request.json) -**Response Schema**: [`https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-response.json) +**Request Schema**: [`/schemas/v1/media-buy/update-media-buy-request.json`](https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-request.json) +**Response Schema**: [`/schemas/v1/media-buy/update-media-buy-response.json`](https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-response.json) ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `media_buy_id` | string | No* | Publisher's ID of the media buy to update | -| `buyer_ref` | string | No* | Buyer's reference for the media buy to update | -| `active` | boolean | No | Pause/resume the entire media buy | -| `start_time` | string | No | New campaign start time: `"asap"` to start as soon as possible, or ISO 8601 date-time for scheduled start | -| `end_time` | string | No | New end date/time in ISO 8601 format (UTC unless timezone specified) | -| `packages` | PackageUpdate[] | No | Package-specific updates (see Package Update Object below) | -| `push_notification_config` | PushNotificationConfig | No | Optional webhook for async update notifications (see Webhook Configuration below) | - -*Either `media_buy_id` or `buyer_ref` must be provided +| `media_buy_id` | string | Yes | Media buy identifier to update | +| `start_time` | string | No | Updated campaign start time | +| `end_time` | string | No | Updated campaign end time | +| `status` | string | No | Change status (`"active"`, `"paused"`, `"cancelled"`) | +| `packages` | PackageUpdate[] | No | Package-level updates (see below) | +| `reporting_webhook` | ReportingWebhook | No | Update or add webhook configuration | ### Package Update Object -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `package_id` | string | No* | Publisher's ID of package to update | -| `buyer_ref` | string | No* | Buyer's reference for the package to update | -| `budget` | number | No | Updated budget allocation for this package in the currency specified by the pricing option | -| `pacing` | string | No | Pacing strategy: `"even"`, `"asap"`, or `"front_loaded"` | -| `bid_price` | number | No | Updated bid price for auction-based pricing options (only applies to auction packages) | -| `active` | boolean | No | Pause/resume specific package | -| `targeting_overlay` | TargetingOverlay | No | Update targeting for this package (see Targeting Overlay Object in create_media_buy) | -| `creative_ids` | string[] | No | Update creative assignments | - -*Either `package_id` or `buyer_ref` must be provided - -## Webhook Configuration (Task-Specific) - -For long-running updates (typically requiring approval workflows), you can provide a task-specific webhook to be notified when the update completes: - -```json -{ - "buyer_ref": "nike_q1_campaign_2024", - "packages": [ - { - "buyer_ref": "nike_ctv_sports_package", - "budget": 150000 - } - ], - "push_notification_config": { - "url": "https://buyer.com/webhooks/media-buy-updates", - "authentication": { - "schemes": ["HMAC-SHA256"], - "credentials": "shared_secret_32_chars" - } - } -} -``` - -**When webhooks are sent:** -- Update requires manual approval (status: `submitted` β†’ `completed`) -- Update takes longer than ~120 seconds (status: `working` β†’ `completed`) - -**Webhook payload:** -- Protocol fields at top-level (operation_id, task_type, status, etc.) -- `result` contains update_media_buy payload, including media_buy_id, affected_packages, implementation_date - -See [Webhook Security](/docs/protocols/core-concepts.mdx#security) for authentication details. - -## Response (Message) +| Parameter | Type | Description | +|-----------|------|-------------| +| `package_id` | string | Package identifier to update | +| `status` | string | Package status (`"active"`, `"paused"`, `"cancelled"`) | +| `budget` | number | Updated budget allocation | +| `pacing` | string | Updated pacing strategy | +| `bid_price` | number | Updated bid price (auction products only) | +| `targeting_overlay` | TargetingOverlay | Updated targeting restrictions | +| `creative_ids` | string[] | Replace assigned creatives | -The response includes a human-readable message that: -- Confirms what changes were made and their impact -- Explains approval requirements if applicable -- Provides context on budget and pacing changes -- Describes when changes take effect +## Response -The message is returned differently in each protocol: -- **MCP**: Returned as a `message` field in the JSON response -- **A2A**: Returned as a text part in the artifact +Returns updated media buy with status: -## Response (Payload) +| Field | Description | +|-------|-------------| +| `media_buy_id` | Media buy identifier | +| `status` | Update status (`"completed"`, `"working"`, `"submitted"`) | +| `packages` | Updated packages with new values | +| `message` | Human-readable update description | -```json -{ - "media_buy_id": "string", - "buyer_ref": "string", - "implementation_date": "string", - "affected_packages": [ - { - "package_id": "string", - "buyer_ref": "string" - } - ] -} -``` +See [schema](https://adcontextprotocol.org/schemas/v1/media-buy/update-media-buy-response.json) for complete field list. -### Field Descriptions - -- **media_buy_id**: Publisher's identifier for the media buy -- **buyer_ref**: Buyer's reference identifier for the media buy -- **implementation_date**: ISO 8601 timestamp when changes take effect -- **affected_packages**: Array of packages that were modified - - **package_id**: Publisher's package identifier - - **buyer_ref**: Buyer's reference for the package - -## Protocol-Specific Examples - -The AdCP payload is identical across protocols. Only the request/response wrapper differs. - -### MCP Request -```json -{ - "tool": "update_media_buy", - "arguments": { - "buyer_ref": "nike_q1_campaign_2024", - "packages": [ - { - "buyer_ref": "nike_ctv_sports_package", - "budget": 100000 - } - ] - } -} -``` +## Common Scenarios -### MCP Response (Synchronous) -```json -{ - "message": "Successfully updated media buy. CTV package budget increased to $100,000.", - "status": "completed", - "media_buy_id": "mb_12345", - "buyer_ref": "nike_q1_campaign_2024", - "implementation_date": "2024-02-01T00:00:00Z", - "affected_packages": [ - { - "package_id": "pkg_12345_001", - "buyer_ref": "nike_ctv_sports_package" - } - ] -} -``` - -### MCP Response (Asynchronous) -```json -{ - "task_id": "task_update_456", - "status": "working", - "message": "Processing media buy update..." -} -``` +### Pause Campaign -### A2A Request - -#### Natural Language Invocation ```javascript -await a2a.send({ - message: { - parts: [{ - kind: "text", - text: "Please update my Nike Q1 campaign budget to $150,000 with front-loaded pacing. Also increase the CTV sports package budget to $100,000." - }] - } -}); -``` +import { testAgent } from '@adcp/client/testing'; -#### Explicit Skill Invocation -```javascript -await a2a.send({ - message: { - parts: [{ - kind: "data", - data: { - skill: "update_media_buy", - parameters: { - buyer_ref: "nike_q1_campaign_2024", - packages: [ - { - buyer_ref: "nike_ctv_sports_package", - budget: 100000 - } - ] - } - } - }] - } +// Pause entire campaign +const result = await testAgent.updateMediaBuy({ + media_buy_id: 'mb_12345', + status: 'paused' }); -``` -### A2A Response (Synchronous) -A2A returns results as artifacts: -```json -{ - "artifacts": [{ - "name": "update_confirmation", - "parts": [ - { - "kind": "text", - "text": "Successfully updated media buy. CTV package budget increased to $100,000." - }, - { - "kind": "data", - "data": { - "media_buy_id": "mb_12345", - "buyer_ref": "nike_q1_campaign_2024", - "implementation_date": "2024-02-01T00:00:00Z", - "affected_packages": [ - {"package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package"} - ] - } - } - ] - }] -} +console.log(`Campaign paused: ${result.status}`); ``` -### A2A Response (Asynchronous with SSE) -```json -{ - "task_id": "task_update_456", - "status": "working", - "updates": "https://ad-server.example.com/sse/task_update_456" -} -``` - -SSE Updates: -``` -event: status -data: {"status": "working", "message": "Validating update parameters..."} +### Update Package Budget -event: status -data: {"status": "working", "message": "Applying budget changes to packages..."} +```javascript +import { testAgent } from '@adcp/client/testing'; + +// Increase budget for specific package +const result = await testAgent.updateMediaBuy({ + media_buy_id: 'mb_12345', + packages: [{ + package_id: 'pkg_67890', + budget: 25000 // Increased from 15000 + }] +}); -event: completed -data: {"artifacts": [{"name": "update_confirmation", "parts": [{"kind": "text", "text": "Successfully updated media buy."}, {"kind": "data", "data": {"media_buy_id": "mb_12345", "buyer_ref": "nike_q1_campaign_2024", "implementation_date": "2024-02-01T00:00:00Z", "affected_packages": [{"package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package"}]}}]}]} +console.log(`Package budget updated: ${result.packages[0].budget}`); ``` -### Key Differences -- **MCP**: Direct tool call with arguments, returns flat JSON response -- **A2A**: Skill invocation with input, returns artifacts with text and data parts -- **Payload**: The `input` field in A2A contains the exact same structure as MCP's `arguments` +### Change Campaign Dates -## Scenarios +```javascript +import { testAgent } from '@adcp/client/testing'; -### Example 1: Campaign Pause +// Extend campaign end date +const result = await testAgent.updateMediaBuy({ + media_buy_id: 'mb_12345', + end_time: '2025-03-31T23:59:59Z' // Extended from original end date +}); -#### Request -```json -{ - "buyer_ref": "purina_pet_campaign_q1", - "active": false -} +console.log(`Campaign extended to: ${result.end_time}`); ``` -#### Response -**Message**: "Campaign paused successfully. All 2 packages have stopped delivering impressions. You've spent $16,875 of your $50,000 budget (33.8%). Campaign can be resumed at any time to continue delivery." - -**Payload**: -```json -{ - "media_buy_id": "gam_1234567890", - "buyer_ref": "purina_pet_campaign_q1", - "implementation_date": "2024-02-08T00:00:00Z", - "affected_packages": [ - {"package_id": "gam_pkg_001", "buyer_ref": "purina_ctv_package"}, - {"package_id": "gam_pkg_002", "buyer_ref": "purina_audio_package"} - ] -} -``` +### Update Targeting -### Example 2: Complex Update - -#### Request -```json -{ - "buyer_ref": "purina_pet_campaign_q1", - "end_time": "2024-02-28T23:59:59Z", - "packages": [ - { - "buyer_ref": "purina_ctv_package", - "budget": 45000, - "pacing": "front_loaded" - }, - { - "buyer_ref": "purina_audio_package", - "active": false +```javascript +import { testAgent } from '@adcp/client/testing'; + +// Add geographic restrictions to package +const result = await testAgent.updateMediaBuy({ + media_buy_id: 'mb_12345', + packages: [{ + package_id: 'pkg_67890', + targeting_overlay: { + geo_country_any_of: ['US', 'CA'], // Expanded from US-only + geo_region_any_of: ['CA', 'NY', 'TX', 'ON', 'QC'] } - ] -} -``` + }] +}); -#### Response - Immediate Update -**Message**: "Campaign updated successfully. CTV package budget increased to $45,000 and switched to front-loaded pacing to allocate more remaining budget earlier in the remaining campaign period. Audio package has been paused. Campaign extended through February 28. Changes take effect immediately." - -**Payload**: -```json -{ - "media_buy_id": "gam_1234567890", - "buyer_ref": "purina_pet_campaign_q1", - "implementation_date": "2024-02-08T00:00:00Z", - "affected_packages": [ - {"package_id": "gam_pkg_001", "buyer_ref": "purina_ctv_package"}, - {"package_id": "gam_pkg_002", "buyer_ref": "purina_audio_package"} - ] -} +console.log('Targeting updated successfully'); ``` -### Example 3: Update Requiring Approval +### Replace Creatives -#### Request -```json -{ - "buyer_ref": "purina_pet_campaign_q1", - "packages": [ - { - "buyer_ref": "purina_ctv_package", - "budget": 150000 - } - ] -} -``` - -#### Response - Pending Approval -**Message**: "CTV package budget increase to $150,000 requires manual approval due to the significant change (+400%). This typically takes 2-4 hours during business hours. Your campaign continues to deliver at the current budget until approved. I'll notify you once the increase is approved." +```javascript +import { testAgent } from '@adcp/client/testing'; + +// Swap out creative assets +const result = await testAgent.updateMediaBuy({ + media_buy_id: 'mb_12345', + packages: [{ + package_id: 'pkg_67890', + creative_ids: ['creative_new_1', 'creative_new_2'] // Replace existing + }] +}); -**Payload**: -```json -{ - "media_buy_id": "gam_1234567890", - "buyer_ref": "purina_pet_campaign_q1", - "implementation_date": null, - "affected_packages": [] -} +console.log(`Assigned ${result.packages[0].creative_ids.length} new creatives`); ``` -## PATCH Semantics - -This tool follows PATCH update semantics: - -- **Only included fields are modified** - Omitted fields remain unchanged -- **Null values clear/reset fields** - Where applicable -- **Packages not mentioned remain unchanged** - Only listed packages are updated +## What Can Be Updated +### Campaign-Level Updates -## Asynchronous Updates +βœ… **Can update:** +- Start/end times (subject to seller approval) +- Campaign status (active/paused/cancelled) +- Reporting webhook configuration -Both MCP and A2A support asynchronous updates for operations that may take time or require approval: +❌ **Cannot update:** +- Media buy ID +- Buyer reference +- Brand manifest +- Original package product IDs -### MCP Asynchronous Flow +### Package-Level Updates -1. Initial request returns immediately with task_id and status "working" -2. Client polls using update_media_buy_status with the task_id -3. Final status includes the complete update results +βœ… **Can update:** +- Budget allocation +- Pacing strategy +- Bid prices (auction products) +- Targeting overlays +- Creative assignments +- Package status -### A2A Asynchronous Flow +❌ **Cannot update:** +- Package ID +- Product ID +- Pricing option ID +- Format IDs (creatives must match existing formats) -1. Initial request returns task_id with SSE URL or webhook configuration -2. Updates stream via SSE or push to webhooks -3. Final event includes complete artifacts with update results +## Update Approval -### Human-in-the-Loop Scenarios +Some updates require seller approval and return `status: "submitted"`: -When updates require approval: +- **Significant budget increases** (threshold varies by seller) +- **Date range changes** affecting inventory availability +- **Targeting changes** that alter campaign scope +- **Creative changes** requiring policy review -```json -{ - "status": "input-required", - "message": "Budget increase requires advertiser approval", - "responsible_party": "advertiser", - "estimated_time": "2-4 hours" +When approval is needed: +```javascript +if (result.status === 'submitted') { + console.log('Update requires approval:', result.message); + // Set up status monitoring } ``` -The system will: -1. Notify the responsible party -2. Maintain current campaign settings -3. Apply changes only after approval -4. Send status updates throughout the process +## PATCH Semantics -## Campaign-Level vs Package-Level Updates +Only specified fields are updated: -The `update_media_buy` tool provides a unified interface that supports both campaign-level and package-level updates in a single call: +```javascript +// This update ONLY changes budget - all other fields unchanged +await agent.updateMediaBuy({ + media_buy_id: 'mb_12345', + packages: [{ + package_id: 'pkg_67890', + budget: 25000 + }] +}); +``` -### Campaign-Level Updates -- `active`: Pause/resume entire campaign -- `budget`: Adjust overall budget configuration -- `start_time`: Change campaign start date/time -- `end_time`: Extend or shorten campaign -- `targeting_overlay`: Update global targeting -- `pacing`: Change delivery strategy +To replace arrays (like creative_ids), provide the complete new array: -### Package-Level Updates -- Apply different changes to multiple packages in one call -- Each package can have different update parameters -- Update multiple packages in one call -- Each package update is processed independently -- Returns immediately on first error +```javascript +// Replaces ALL creatives with new list +packages: [{ + package_id: 'pkg_67890', + creative_ids: ['new_creative_1', 'new_creative_2'] +}] +``` -## Status Checking +## Error Handling -### MCP Status Checking +| Error Code | Description | Resolution | +|------------|-------------|------------| +| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `INVALID_REQUEST` | Invalid field values | Check request against schema | +| `MEDIA_BUY_NOT_FOUND` | Media buy doesn't exist | Verify media_buy_id | +| `PACKAGE_NOT_FOUND` | Package doesn't exist | Verify package_id | +| `UPDATE_NOT_ALLOWED` | Field cannot be changed | See "What Can Be Updated" above | +| `POLICY_VIOLATION` | Update violates content policy | Review policy message | +| `BUDGET_INSUFFICIENT` | New budget below minimum | Increase budget amount | -For MCP implementations, use the `update_media_buy_status` endpoint to check the status of an asynchronous media buy update. +## Async Workflows -#### Request -```json -{ - "task_id": "task_update_456" // Required - from update_media_buy response -} -``` +Updates may be asynchronous, especially with seller approval: -#### Response Examples - -**Processing:** -```json -{ - "message": "Media buy update in progress - applying changes", - "task_id": "task_update_456", - "status": "working", - "progress": { - "completed": 1, - "total": 2, - "unit_type": "packages", - "responsible_party": "system" - } -} -``` +```javascript +const result = await agent.updateMediaBuy({...}); -**Completed:** -```json -{ - "message": "Successfully updated media buy", - "task_id": "task_update_456", - "status": "completed", - "media_buy_id": "mb_12345", - "buyer_ref": "nike_q1_campaign_2024", - "implementation_date": "2024-02-08T00:00:00Z", - "affected_packages": [ - {"package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package"} - ] +if (result.status === 'working') { + // Poll for completion after a few seconds + await new Promise(resolve => setTimeout(resolve, 5000)); + // Check status via protocol-specific mechanism } -``` -**Pending Approval:** -```json -{ - "message": "Media buy update requires approval. Finance team must approve budget increase.", - "task_id": "task_update_456", - "status": "input-required", - "responsible_party": "advertiser" +if (result.status === 'submitted') { + // Manual approval required (hours to days) + console.log('Awaiting seller approval:', result.message); } ``` -### A2A Status Checking - -For A2A implementations, task status is delivered via: -1. **Polling**: Client can poll using the task_id -2. **Server-Sent Events (SSE)**: Real-time updates via the `updates` URL -3. **Webhooks**: Push notifications to registered endpoints - -## Usage Notes - -- Updates typically take effect immediately unless approval is required -- Budget increases may require approval based on publisher policies -- Pausing a campaign preserves all settings and can be resumed anytime -- Package-level updates override campaign-level settings -- Some updates may affect how remaining budget is allocated over remaining time - -## Platform Implementation +See [Task Management](/docs/protocols/task-management) for protocol-specific async patterns. -How updates map to different platforms: +## Best Practices -- **GAM**: Maps to Order and LineItem updates -- **Kevel**: Maps to Campaign and Flight updates -- **Triton**: Maps to Campaign and Flight updates +**1. Use Precise Updates** +Update only what needs to change - don't resend unchanged values. -## Error Handling - -All update operations return a standardized response: - -```json -{ - "status": "completed" | "failed" | "working" | "rejected", - "implementation_date": "2024-01-20T10:00:00Z", // When change takes effect - "reason": "Error description if failed", - "detail": "Additional context or task ID for pending states" -} -``` +**2. Budget Increases** +Small incremental increases are more likely to be auto-approved than large jumps. -### Task States +**3. Pause Before Major Changes** +Pause campaigns before making significant targeting or creative changes. -Updates follow standard A2A task states: +**4. Test with Small Changes** +Test update workflows with minor changes before critical campaign modifications. -**Normal Flow States:** -- `working`: Update is being processed -- `input-required`: Awaiting approval or additional information -- `completed`: Update successfully applied +**5. Monitor Status** +Always check response status and message for approval requirements or errors. -**Error States:** -- `failed`: Update could not be completed -- `rejected`: Update was rejected by approver -- `cancelled`: Update was cancelled before completion +## Next Steps -## Usage Notes +After updating a media buy: -- Updates may require platform approval depending on the changes -- Budget increases typically process immediately -- Budget decreases may have restrictions based on delivered spend -- Pausing takes effect at the next delivery opportunity -- Campaign extensions require sufficient remaining budget -- Creative updates only affect future impressions -- Some platforms may limit which fields can be updated after activation -- When updating budgets, the system automatically recalculates impression goals based on the package's CPM rate +1. **Verify Changes**: Use [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery) to confirm updates +2. **Upload New Creatives**: Use [`sync_creatives`](/docs/media-buy/task-reference/sync_creatives) if creative_ids changed +3. **Monitor Performance**: Track impact of changes on campaign metrics +4. **Optimize Further**: Use [`provide_performance_feedback`](/docs/media-buy/task-reference/provide_performance_feedback) for ongoing optimization -## Design Note +## Learn More -Adding new packages post-creation is not yet supported. This functionality is under consideration for a future version. \ No newline at end of file +- [Media Buy Lifecycle](/docs/media-buy/media-buys/) - Complete campaign workflow +- [Targeting](/docs/media-buy/advanced-topics/targeting) - Targeting overlays and restrictions +- [Task Management](/docs/protocols/task-management) - Async patterns and status checking +- [create_media_buy](/docs/media-buy/task-reference/create_media_buy) - Initial campaign creation diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index d1391f44..441e19fc 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -1,357 +1,196 @@ --- sidebar_position: 2 title: Quickstart Guide -description: Get started with AdCP in 5 minutes - discover products, test the protocol, and understand authentication -keywords: [adcp quickstart, getting started, testing, authentication, adcp tutorial] +description: Get started with AdCP in 5 minutes +keywords: [adcp quickstart, getting started, adcp tutorial] --- -# AdCP Quickstart Guide +# AdCP Quickstart -Get started with AdCP in 5 minutes. This guide shows you how to test AdCP, understand authentication, and make your first requests. +Get started with AdCP in 5 minutes using our public test agent. -## Try AdCP Right Now +## Interactive Testing -### Interactive Testing Platform +Try AdCP without writing code: **[testing.adcontextprotocol.org](https://testing.adcontextprotocol.org)** -The fastest way to explore AdCP is through our **interactive testing platform**: +## Code Examples -πŸ”— **[https://testing.adcontextprotocol.org](https://testing.adcontextprotocol.org)** +Install the client library: -This platform lets you: -- Test all AdCP tasks interactively -- See real request/response examples -- Validate your understanding of the protocol -- Try different scenarios without writing code - -### Test Agent for Development - -For developers building AdCP integrations, we provide a **public test agent** with free credentials: +```bash +npm install @adcp/client # JavaScript/TypeScript +pip install adcp # Python +``` -**Agent URL**: `https://test-agent.adcontextprotocol.org` +Discover products from the test agent: -**Agent Card**: [https://test-agent.adcontextprotocol.org/.well-known/agent-card.json](https://test-agent.adcontextprotocol.org/.well-known/agent-card.json) +**JavaScript:** -**Free Test Credentials**: +```javascript +import { ADCPMultiAgentClient } from '@adcp/client'; -**For MCP Protocol**: -```json -{ - "agent_uri": "https://test-agent.adcontextprotocol.org/mcp", - "protocol": "mcp", - "version": "1.0", - "auth": { - "type": "bearer", - "token": "1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ" +const client = new ADCPMultiAgentClient([{ + id: 'test-agent', + agent_uri: 'https://test-agent.adcontextprotocol.org/mcp', + protocol: 'mcp', + auth: { + type: 'bearer', + token: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' } -} -``` - -**For A2A Protocol**: -```json -{ - "agent_uri": "https://test-agent.adcontextprotocol.org/a2a", - "protocol": "a2a", - "version": "1.0", - "auth": { - "type": "bearer", - "token": "L4UCklW_V_40eTdWuQYF6HD5GWeKkgV8U6xxK-jwNO8" +}]); + +const agent = client.agent('test-agent'); +const result = await agent.getProducts({ + brief: 'Premium athletic footwear with innovative cushioning', + brand_manifest: { + name: 'Nike', + url: 'https://nike.com' } +}); + +if (result.success && result.data) { + console.log(`βœ“ Found ${result.data.products.length} matching products`); +} else if (result.error) { + console.log(`Error: ${result.error}`); +} else { + console.log(`Status: ${result.status}`); } ``` -**What You Can Do**: -- βœ… Test all AdCP tasks with authentication -- βœ… See complete product catalogs with pricing -- βœ… Create test media buys (with dry run mode) -- βœ… Upload and sync test creatives -- βœ… Practice integration patterns - -**Important**: This is a test agent. Data is ephemeral and may be reset. Use dry run mode to avoid creating actual test campaigns. - -## Understanding Authentication - -AdCP uses a tiered authentication model - some operations work without credentials, while others require authentication. - -### Operations That DON'T Require Authentication - -These **capability discovery** operations work without credentials: - -- βœ… **`list_creative_formats`** - Browse available creative formats -- βœ… **`list_authorized_properties`** - See which properties an agent represents -- βœ… **`get_products`** - Discover inventory (limited results without auth) - -**Why these are public**: Publishers want potential buyers to discover their inventory and understand their capabilities before establishing a relationship. - -**Important**: Unauthenticated `get_products` requests may return: -- Limited product catalog -- No pricing information -- Generic products only -- No custom offerings - -### Operations That REQUIRE Authentication - -These operations require valid credentials: - -- πŸ”’ **`get_products`** (full results) - See complete catalog with pricing -- πŸ”’ **`create_media_buy`** - Create advertising campaigns -- πŸ”’ **`update_media_buy`** - Modify existing campaigns -- πŸ”’ **`sync_creatives`** - Upload creative assets -- πŸ”’ **`list_creatives`** - View your creative library -- πŸ”’ **`get_media_buy_delivery`** - Monitor campaign performance -- πŸ”’ **`provide_performance_feedback`** - Submit optimization signals - -**Why authentication is required**: These operations involve financial commitments, access to proprietary data, or modifications to campaigns. - -## Getting Credentials - -To access authenticated operations, you need to establish an account with each sales agent you want to work with. - -### How to Get Access - -**1. Contact the Sales Agent** - -Each AdCP sales agent manages their own accounts and credentials. Find their contact information via: - -- The agent's website (discovered via `adagents.json`) -- Direct outreach to the publisher -- Through aggregation platforms (like Scope3) - -**2. Credential Types** - -Sales agents typically support one or both: - -- **API Keys**: Simple header-based authentication (`X-API-Key: `) -- **JWT Tokens**: OAuth-based authentication (`Authorization: Bearer `) - -See [Authentication Reference](/docs/reference/authentication) for technical details. - -**3. Dynamic Registration (Optional)** - -Some sales agents support OAuth-based dynamic registration - check their documentation or `adagents.json` file for details. - -### Working with Multiple Agents - -**Important**: You need separate credentials for each sales agent. - -``` -Your App -β”œβ”€β”€ Sales Agent A (ESPN inventory) -β”‚ └── Requires ESPN credentials -β”œβ”€β”€ Sales Agent B (Weather.com inventory) -β”‚ └── Requires Weather.com credentials -└── Sales Agent C (Multi-publisher network) - └── Requires network credentials +**Python:** + +```python +import asyncio +from adcp import ADCPMultiAgentClient, AgentConfig, GetProductsRequest + +async def main(): + client = ADCPMultiAgentClient([ + AgentConfig( + id='test-agent', + agent_uri='https://test-agent.adcontextprotocol.org/mcp', + protocol='mcp', + auth_token='1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' + ) + ]) + + agent = client.agent('test-agent') + result = await agent.get_products( + GetProductsRequest( + brief='Premium athletic footwear with innovative cushioning', + brand_manifest={ + 'name': 'Nike', + 'url': 'https://nike.com' + } + ) + ) + + if result.success and result.data: + print(f"βœ“ Found {len(result.data.products)} matching products") + elif result.error: + print(f"Error: {result.error}") + else: + print(f"Status: {result.status}") + +asyncio.run(main()) ``` -**Aggregation Platforms**: Consider using platforms like Scope3 that manage credentials and relationships with multiple sales agents on your behalf. - -## Your First Request - -Let's make a test request using the public test agent with authentication. - -### Step 1: Test with Authentication - -Use the test agent credentials provided above to make an authenticated request: - -**Using MCP (JSON-RPC 2.0)**: +**CLI:** ```bash -curl -X POST https://test-agent.adcontextprotocol.org/mcp \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ" \ - -H "Accept: application/json, text/event-stream" \ - -d '{ - "jsonrpc": "2.0", - "id": "req-123", - "method": "tools/call", - "params": { - "name": "get_products", - "arguments": { - "promoted_offering": "Nike Air Max 2024 - latest innovation in cushioning technology" - } - } - }' +# Using npx (JavaScript/Node.js) +npx @adcp/client \ + https://test-agent.adcontextprotocol.org/mcp \ + get_products \ + '{"brief":"Premium athletic footwear with innovative cushioning","brand_manifest":{"name":"Nike","url":"https://nike.com"}}' \ + --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ + +# Or using uvx (Python) +uvx adcp \ + https://test-agent.adcontextprotocol.org/mcp \ + get_products \ + '{"brief":"Premium athletic footwear with innovative cushioning","brand_manifest":{"name":"Nike","url":"https://nike.com"}}' \ + --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ ``` -**Expected Response** (Server-Sent Events format): - -``` -event: message -data: {"status":"success","products":[...],"message":"Found 3 products..."} -``` +**Test agent credentials** (free to use): +- **Agent URL**: `https://test-agent.adcontextprotocol.org/mcp` +- **Auth token**: `1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ` -### Step 2: Interpret the Response +## Understanding Authentication -The response includes: -- **`message`**: Human-readable summary of results -- **`products`**: Array of available inventory products with pricing information -- **`context_id`**: Session identifier for follow-up requests +Some operations work without credentials: +- `list_creative_formats` - Browse creative formats +- `list_authorized_properties` - See publisher properties +- `get_products` - Discover inventory (limited results) -**With authentication**, you'll see: -- βœ… Complete product catalog -- βœ… Pricing information (CPM, min_spend) -- βœ… Custom product offerings -- βœ… Measurement capabilities +Most operations require authentication: +- `get_products` (full results) - Complete catalog with pricing +- `create_media_buy` - Create campaigns +- `sync_creatives` - Upload creatives +- `get_media_buy_delivery` - Monitor performance -### Step 3: Try Without Authentication +[Learn more about authentication β†’](/docs/reference/authentication) -Compare by making the same request **without** the Authorization header: +## Getting Production Credentials -```bash -curl -X POST https://test-agent.adcontextprotocol.org/mcp \ - -H "Content-Type: application/json" \ - -H "Accept: application/json, text/event-stream" \ - -d '{ - "jsonrpc": "2.0", - "id": "req-123", - "method": "tools/call", - "params": { - "name": "get_products", - "arguments": { - "promoted_offering": "Nike Air Max 2024 - latest innovation in cushioning technology" - } - } - }' -``` +To work with real publishers: -You'll notice fewer products and missing pricing details - this demonstrates the tiered authentication model. +1. **Contact the sales agent** - Find contact info via their agent card (`.well-known/agent-card.json`) +2. **Request credentials** - Most agents provide API keys or JWT tokens +3. **Store securely** - Never commit credentials to version control -## Testing with Dry Run Mode +You need separate credentials for each sales agent you work with. -AdCP supports comprehensive testing without spending real money or affecting production systems. +## Dry Run Mode -### Enable Dry Run Mode +Test without spending money by adding the `X-Dry-Run: true` header: -Add the `X-Dry-Run` header to any request: - -```bash -curl -X POST \ - -H "X-Dry-Run: true" \ - -H "Authorization: Bearer " \ - -d '{...}' +```javascript +const result = await agent.createMediaBuy(request, { dryRun: true }); ``` -**What Dry Run Does**: -- βœ… Validates your request structure -- βœ… Returns realistic simulated responses -- βœ… Tests error scenarios -- ❌ Does NOT create real campaigns -- ❌ Does NOT spend actual money -- ❌ Does NOT affect production systems - -**Learn more**: [Testing & Development Guide](/docs/media-buy/advanced-topics/testing) - -## Protocol Choice: MCP vs A2A - -AdCP tasks work identically across protocols - choose based on your technical needs: - -### Use MCP if: -- You're integrating with Claude or MCP-compatible AI assistants -- You prefer direct tool-calling patterns -- Your client already supports MCP - -### Use A2A if: -- You're using Google's agent ecosystem -- You prefer message-based interactions with Server-Sent Events -- Your client already supports A2A - -**The tasks are the same** - `get_products`, `create_media_buy`, etc. work identically in both protocols, just with different request/response wrappers. - -**Learn more**: [Protocol Comparison](/docs/protocols/protocol-comparison) - -## Using the NPM Client - -If you're building in Node.js, use the official AdCP client: +Dry run mode validates requests and returns simulated responses without creating real campaigns. -```bash -npm install @adcp/client -``` +[Learn more about testing β†’](/docs/media-buy/advanced-topics/testing) -**Example usage with test agent**: +## Protocol Choice -```javascript -import { AdcpClient } from '@adcp/client'; +AdCP works over MCP or A2A protocols. The tasks are identical - choose based on your integration: -// Use the public test agent -const client = new AdcpClient({ - agentUrl: 'https://test-agent.adcontextprotocol.org/mcp', - protocol: 'mcp', - bearerToken: '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ' // Test agent token -}); +- **MCP** - For Claude and MCP-compatible clients +- **A2A** - For Google's agent ecosystem -// Test authenticated product discovery -const products = await client.getProducts({ - promoted_offering: 'Nike Air Max 2024 - innovative cushioning technology' -}); +[Compare protocols β†’](/docs/protocols/protocol-comparison) -console.log(`Found ${products.products.length} products`); -console.log('First product:', products.products[0]); - -// Test with dry run mode -const mediaBuy = await client.createMediaBuy({ - product_id: products.products[0].product_id, - budget: 10000, - start_date: '2025-11-01', - end_date: '2025-11-30' -}, { - dryRun: true // No actual campaign created -}); +## Next Steps -console.log('Test media buy created:', mediaBuy.media_buy_id); -``` +- **[Media Buy Workflow](/docs/media-buy/)** - Create and manage campaigns +- **[Task Reference](/docs/media-buy/task-reference/)** - Complete API documentation ## Common Issues ### "Invalid request parameters" Error -**Problem**: You're sending incorrect parameters or using an outdated schema. - -**Solution**: -1. Verify you're using the latest AdCP version (check [schema registry](https://adcontextprotocol.org/schemas/v1/index.json)) -2. Check parameter names match the [task reference docs](/docs/media-buy/task-reference/get_products) -3. Use the testing platform to validate your request structure - -### Missing Pricing Information +Check that your request includes required fields. Each task has specific requirements. -**Problem**: Products return without CPM or pricing details. +[See task reference β†’](/docs/media-buy/task-reference) -**Solution**: You need to authenticate. Unauthenticated requests only return limited public information. +### "Unauthorized" Response -### "Authentication required" Error +Verify: +- Auth header is included: `Authorization: Bearer ` +- Token is valid and not expired +- Agent requires authentication for this operation -**Problem**: Trying to access an authenticated operation without credentials. +### Async Task Status -**Solution**: [Get credentials](#getting-credentials) from the sales agent, then include them in your request headers. +Some operations return immediately with a `task_id` for long-running work: -## Next Steps - -Now that you understand the basics: - -1. **Explore the Protocol**: - - Try the [interactive testing platform](https://testing.adcontextprotocol.org) - - Read the [MCP Guide](/docs/protocols/mcp-guide) or [A2A Guide](/docs/protocols/a2a-guide) - - Review the [Task Reference](/docs/media-buy/task-reference) - -2. **Get Credentials**: - - Identify sales agents you want to work with (check their `adagents.json` files) - - Contact them to establish accounts - - Test authenticated operations - -3. **Build Your Integration**: - - Install the [@adcp/client](https://www.npmjs.com/package/@adcp/client) NPM package - - Follow the [implementation patterns](/docs/media-buy/advanced-topics/orchestrator-design) - - Use [dry run mode](/docs/media-buy/advanced-topics/testing) to test safely - -4. **Join the Community**: - - [Slack Community](https://join.slack.com/t/agenticads/shared_invite/zt-3c5sxvdjk-x0rVmLB3OFHVUp~WutVWZg) - - [GitHub Discussions](https://github.com/adcontextprotocol) - - Email: support@adcontextprotocol.org - -## Key Takeaways +```javascript +if (result.status === 'pending') { + // Check status later or provide webhook + console.log(`Task ID: ${result.task_id}`); +} +``` -βœ… **Test without code** - Use https://testing.adcontextprotocol.org -βœ… **Discovery is public** - list_creative_formats, list_authorized_properties, and basic get_products work without auth -βœ… **Full access needs credentials** - Contact each sales agent to get accounts -βœ… **Dry run mode** - Test safely without spending money -βœ… **Protocol agnostic** - Same tasks work in MCP and A2A -βœ… **NPM client available** - Use @adcp/client for Node.js projects +[Learn about async operations β†’](/docs/media-buy/advanced-topics/) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index c431dfda..46bb045b 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -93,30 +93,14 @@ const config: Config = { }, ], - [ - '@docusaurus/plugin-client-redirects', - { - redirects: [ - { - to: '/docs/intro/', - from: '/docs', - }, - ], - }, - ], + // No redirects needed - docs served by Mintlify at docs.adcontextprotocol.org ], presets: [ [ 'classic', { - docs: { - sidebarPath: './sidebars.ts', - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: - 'https://github.com/adcontextprotocol/adcp/tree/main/', - }, + docs: false, // Docs served by Mintlify at docs.adcontextprotocol.org sitemap: { changefreq: 'weekly', priority: 0.5, diff --git a/package-lock.json b/package-lock.json index f6cda56f..7698a61a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-dom": "^18.0.0" }, "devDependencies": { + "@adcp/client": "^3.0.0", "@changesets/cli": "^2.29.7", "@docusaurus/module-type-aliases": "^3.9.1", "@docusaurus/tsconfig": "^3.9.1", @@ -35,6 +36,48 @@ "node": ">=18.0" } }, + "node_modules/@a2a-js/sdk": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.5.tgz", + "integrity": "sha512-6xAApkiss2aCbJXmXLC845tifcbYJ/R4Dj22kQsOaanMbf9bvkYhebDEuYPAIu3aaR5MWaBqG7OCK3IF8dqZZQ==", + "dev": true, + "peer": true, + "dependencies": { + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, + "node_modules/@adcp/client": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@adcp/client/-/client-3.0.0.tgz", + "integrity": "sha512-4rFEpVIwr6L5L/ZbkVN2OfU3a2SVXllX5jpFISOWqv8lWXB1jC00S8OEM1WXmL3vJdwYhCb84O5NZs/z6/dBmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.4.1", + "dotenv": "^17.2.2" + }, + "bin": { + "adcp": "bin/adcp.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^0.3.4", + "@modelcontextprotocol/sdk": "^1.17.5" + } + }, "node_modules/@ai-sdk/gateway": { "version": "1.0.30", "license": "Apache-2.0", @@ -6685,6 +6728,390 @@ "zod": "^3.24.1" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz", + "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -9596,6 +10023,21 @@ "node": ">=4" } }, + "node_modules/better-sqlite3": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", + "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, "node_modules/big.js": { "version": "5.2.2", "license": "MIT", @@ -9613,6 +10055,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.3", "license": "MIT", @@ -12174,6 +12638,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "license": "MIT", @@ -12851,6 +13328,20 @@ "bare-events": "^2.7.0" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "license": "MIT", @@ -12879,6 +13370,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "license": "MIT", @@ -12923,6 +13424,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -13207,6 +13725,13 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "license": "MIT", @@ -13394,6 +13919,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.1", "license": "MIT", @@ -13626,6 +14158,13 @@ "node": ">= 14" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/github-slugger": { "version": "1.5.0", "license": "ISC" @@ -15363,6 +15902,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -18347,6 +18894,13 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.0", "license": "MIT", @@ -18438,6 +18992,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "license": "MIT", @@ -18534,6 +19095,19 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.80.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz", + "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "license": "MIT", @@ -19364,6 +19938,17 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "license": "MIT", @@ -20865,6 +21450,70 @@ "postcss": "^8.4.31" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prettier": { "version": "2.8.8", "dev": true, @@ -22282,6 +22931,36 @@ "points-on-path": "^0.2.1" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "license": "MIT", @@ -22955,6 +23634,27 @@ "version": "3.0.7", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/simple-eval": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", @@ -22968,6 +23668,32 @@ "node": ">=12" } }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -24278,6 +25004,19 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/twoslash": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.3.4.tgz", diff --git a/package.json b/package.json index e9e4b647..2af05045 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ "typecheck": "tsc", "test:schemas": "node tests/schema-validation.test.js", "test:examples": "node tests/example-validation-simple.test.js", + "test:snippets": "node tests/snippet-validation.test.js", "generate-openapi": "node scripts/generate-openapi.js", "test": "npm run test:schemas && npm run test:examples && npm run typecheck", + "test:all": "npm run test:schemas && npm run test:examples && npm run test:snippets && npm run typecheck", "precommit": "npm test", "prepare": "husky", "changeset": "changeset", @@ -41,6 +43,7 @@ "react-dom": "^18.0.0" }, "devDependencies": { + "@adcp/client": "^3.0.0", "@changesets/cli": "^2.29.7", "@docusaurus/module-type-aliases": "^3.9.1", "@docusaurus/tsconfig": "^3.9.1", diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..16301dd4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "adcp-docs" +version = "0.1.0" +description = "AdCP Documentation Dependencies" +requires-python = ">=3.11" +dependencies = [ + "adcp>=1.4.0", +] + +[tool.hatch.build.targets.wheel] +packages = [] diff --git a/scripts/analyze-snippets.js b/scripts/analyze-snippets.js new file mode 100755 index 00000000..cd13c912 --- /dev/null +++ b/scripts/analyze-snippets.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * Analyze code snippets across documentation to identify untested blocks + */ + +const fs = require('fs'); +const path = require('path'); + +const DOCS_BASE_DIR = path.join(__dirname, '../docs'); + +function findDocFiles(dir = DOCS_BASE_DIR) { + let files = []; + const items = fs.readdirSync(dir); + + for (const item of items) { + const fullPath = path.join(dir, item); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + files = files.concat(findDocFiles(fullPath)); + } else if (item.endsWith('.md') || item.endsWith('.mdx')) { + files.push(fullPath); + } + } + + return files; +} + +function analyzeFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const relativePath = path.relative(DOCS_BASE_DIR, filePath); + + // Find all code blocks + const codeBlockRegex = /```(\w+)([^\n]*)\n([\s\S]*?)```/g; + let match; + let totalBlocks = 0; + let testedBlocks = 0; + let languages = new Set(); + + while ((match = codeBlockRegex.exec(content)) !== null) { + const language = match[1]; + const metadata = match[2]; + const isTested = /\btest=true\b/.test(metadata) || /\btestable\b/.test(metadata); + + totalBlocks++; + if (isTested) testedBlocks++; + + if (['javascript', 'typescript', 'python', 'bash', 'sh'].includes(language.toLowerCase())) { + languages.add(language.toLowerCase()); + } + } + + return { + path: relativePath, + totalBlocks, + testedBlocks, + untestedBlocks: totalBlocks - testedBlocks, + languages: Array.from(languages), + hasMixedLanguages: languages.size > 1 + }; +} + +const files = findDocFiles(); +const results = files.map(analyzeFile).filter(r => r.totalBlocks > 0); + +// Sort by untested blocks +results.sort((a, b) => b.untestedBlocks - a.untestedBlocks); + +console.log('\nπŸ“Š Documentation Snippet Analysis\n'); +console.log('Top 15 files with untested code snippets:\n'); +results.slice(0, 15).forEach((r, i) => { + console.log(`${(i + 1)}. ${r.path}`); + console.log(` Total: ${r.totalBlocks}, Tested: ${r.testedBlocks}, Untested: ${r.untestedBlocks}`); + if (r.languages.length > 0) { + console.log(` Languages: ${r.languages.join(', ')}`); + } + console.log(''); +}); + +const totals = results.reduce((acc, r) => ({ + totalBlocks: acc.totalBlocks + r.totalBlocks, + testedBlocks: acc.testedBlocks + r.testedBlocks +}), { totalBlocks: 0, testedBlocks: 0 }); + +console.log(`\nπŸ“ˆ Overall Statistics:`); +console.log(`Total code blocks: ${totals.totalBlocks}`); +console.log(`Tested: ${totals.testedBlocks} (${(totals.testedBlocks / totals.totalBlocks * 100).toFixed(1)}%)`); +console.log(`Untested: ${totals.totalBlocks - totals.testedBlocks} (${((totals.totalBlocks - totals.testedBlocks) / totals.totalBlocks * 100).toFixed(1)}%)`); diff --git a/scripts/check-testable-snippets.js b/scripts/check-testable-snippets.js new file mode 100755 index 00000000..ec295a0f --- /dev/null +++ b/scripts/check-testable-snippets.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node +/** + * Check for untested code snippets in git diff + * + * This script checks staged changes for new code blocks that aren't marked + * as testable. It's designed to run as a pre-commit hook to ensure new + * examples are tested. + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); + +// Get the list of staged files +const stagedFiles = execSync('git diff --cached --name-only --diff-filter=AM', { encoding: 'utf8' }) + .split('\n') + .filter(file => file.endsWith('.md') || file.endsWith('.mdx')); + +if (stagedFiles.length === 0) { + console.log('βœ“ No documentation files changed'); + process.exit(0); +} + +// Languages that should be tested +const TESTABLE_LANGUAGES = ['javascript', 'typescript', 'python', 'bash', 'sh', 'shell']; + +// Get diff for each file +let newUntestedSnippets = []; + +for (const file of stagedFiles) { + if (!file) continue; + + try { + const diff = execSync(`git diff --cached -U0 ${file}`, { encoding: 'utf8' }); + + // Find new code blocks (lines starting with +```language) + const lines = diff.split('\n'); + let inAddedBlock = false; + let currentSnippet = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check for new code block + if (line.startsWith('+```')) { + const match = line.match(/^\+```(\w+)(.*)$/); + if (match) { + const language = match[1]; + const metadata = match[2]; + + // Check if it's a testable language + if (TESTABLE_LANGUAGES.includes(language.toLowerCase())) { + // Check if it has test=true marker + const hasTestMarker = /\btest=true\b/.test(metadata) || /\btestable\b/.test(metadata); + + if (!hasTestMarker) { + currentSnippet = { + file, + language, + line: i + 1, + isComplete: false + }; + inAddedBlock = true; + } + } + } + } + + // Track if this looks like a complete example (has imports/requires) + if (inAddedBlock && currentSnippet) { + if (line.match(/^\+(import|from|require|const|let|var|function|def|class|async)/)) { + currentSnippet.isComplete = true; + } + } + + // End of code block + if (inAddedBlock && line.startsWith('+```') && currentSnippet && currentSnippet.line !== i + 1) { + // Only warn about complete-looking examples + if (currentSnippet.isComplete) { + newUntestedSnippets.push(currentSnippet); + } + inAddedBlock = false; + currentSnippet = null; + } + } + } catch (error) { + // File might not exist in previous commit (new file) + if (!error.message.includes('exists on disk, but not in')) { + console.error(`Warning: Could not check ${file}:`, error.message); + } + } +} + +if (newUntestedSnippets.length === 0) { + console.log('βœ“ No new untested code snippets found'); + process.exit(0); +} + +// Report findings +console.log('\n⚠️ Found new untested code snippets:\n'); +for (const snippet of newUntestedSnippets) { + console.log(` ${snippet.file}:${snippet.line} (${snippet.language})`); +} + +console.log('\nπŸ’‘ Consider marking these snippets as testable:'); +console.log(' Add "test=true" after the language identifier:'); +console.log(' ```javascript test=true\n'); +console.log('πŸ“– See docs/contributing/testable-snippets.md for guidelines\n'); + +// Exit with warning (0) rather than error (1) so commit isn't blocked +// This is a soft warning to encourage testing, not a hard requirement +process.exit(0); diff --git a/src/pages/advertising-automation-api.tsx b/src/pages/advertising-automation-api.tsx index 2d64dfc6..5e182e47 100644 --- a/src/pages/advertising-automation-api.tsx +++ b/src/pages/advertising-automation-api.tsx @@ -143,9 +143,9 @@ export default function AdvertisingAutomationAPI() {
- Get Started with AdCP diff --git a/src/pages/api-comparison.tsx b/src/pages/api-comparison.tsx index e32815bc..dbca07cf 100644 --- a/src/pages/api-comparison.tsx +++ b/src/pages/api-comparison.tsx @@ -249,9 +249,9 @@ const campaign = await adcp.create_media_buy({
- Get Started with AdCP diff --git a/src/pages/mcp-advertising-integration.tsx b/src/pages/mcp-advertising-integration.tsx index a25e69ef..0fb9b845 100644 --- a/src/pages/mcp-advertising-integration.tsx +++ b/src/pages/mcp-advertising-integration.tsx @@ -186,15 +186,15 @@ export default function MCPAdvertisingIntegration() {
- MCP Integration Guide - Get Started diff --git a/src/pages/programmatic-advertising-protocol.tsx b/src/pages/programmatic-advertising-protocol.tsx index 2c968f1d..5dc8b637 100644 --- a/src/pages/programmatic-advertising-protocol.tsx +++ b/src/pages/programmatic-advertising-protocol.tsx @@ -194,15 +194,15 @@ export default function ProgrammaticAdvertisingProtocol() {
- Start Building on AdCP - Programmatic API Docs diff --git a/tests/snippet-validation.test.js b/tests/snippet-validation.test.js new file mode 100755 index 00000000..39b25d5c --- /dev/null +++ b/tests/snippet-validation.test.js @@ -0,0 +1,468 @@ +#!/usr/bin/env node +/** + * Documentation Snippet Validation Test Suite + * + * This test suite extracts and validates code snippets from documentation files. + * It ensures that examples in the documentation are functional and accurate. + * + * Snippet Marking Convention: + * - Add 'test=true' or 'testable' after the language identifier to mark snippets for testing + * - Example: ```javascript test=true + * - Example: ```bash testable + * + * Test Agent Configuration: + * - Uses https://test-agent.adcontextprotocol.org for testing + * - MCP token: 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ + * - A2A token: L4UCklW_V_40eTdWuQYF6HD5GWeKkgV8U6xxK-jwNO8 + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const glob = require('glob'); + +const execAsync = promisify(exec); + +// Configuration +const DOCS_BASE_DIR = path.join(__dirname, '../docs'); +const TEST_AGENT_URL = 'https://test-agent.adcontextprotocol.org'; +const MCP_TOKEN = '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ'; +const A2A_TOKEN = 'L4UCklW_V_40eTdWuQYF6HD5GWeKkgV8U6xxK-jwNO8'; + +// Test statistics +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; +let skippedTests = 0; + +// Logging utilities +function log(message, type = 'info') { + const colors = { + info: '\x1b[0m', + success: '\x1b[32m', + error: '\x1b[31m', + warning: '\x1b[33m', + dim: '\x1b[2m' + }; + console.log(`${colors[type]}${message}\x1b[0m`); +} + +/** + * Extract code blocks from markdown/mdx files + * @param {string} filePath - Path to the markdown file + * @returns {Array} Array of code block objects + */ +function extractCodeBlocks(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const blocks = []; + + // Check if page has testable: true in frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const isTestablePage = frontmatterMatch && /testable:\s*true/i.test(frontmatterMatch[1]); + + // Regex to match code blocks with optional metadata + const codeBlockRegex = /```(\w+)([^\n]*)\n([\s\S]*?)```/g; + + let match; + let blockIndex = 0; + + while ((match = codeBlockRegex.exec(content)) !== null) { + const language = match[1]; + const metadata = match[2]; + const code = match[3]; + + // Test if: + // 1. Page has testable: true in frontmatter, OR + // 2. Individual block has test=true or testable marker (legacy) + const shouldTest = isTestablePage || + /\btest=true\b/.test(metadata) || + /\btestable\b/.test(metadata); + + blocks.push({ + file: filePath, + language, + shouldTest, + code: code.trim(), + index: blockIndex++, + line: content.substring(0, match.index).split('\n').length + }); + } + + return blocks; +} + +/** + * Find all documentation files + */ +function findDocFiles() { + return glob.sync('**/*.{md,mdx}', { + cwd: DOCS_BASE_DIR, + absolute: true + }); +} + +/** + * Test a JavaScript/TypeScript snippet + */ +async function testJavaScriptSnippet(snippet) { + // Detect ESM syntax and use .mjs extension to avoid Node warnings + const hasESMSyntax = snippet.code.includes('import ') || snippet.code.includes('export '); + const extension = hasESMSyntax ? '.mjs' : '.js'; + const tempFile = path.join(__dirname, `temp-snippet-${Date.now()}${extension}`); + + try { + // Write snippet to temporary file + fs.writeFileSync(tempFile, snippet.code); + + // Execute with Node.js from project root to access node_modules + const { stdout, stderr } = await execAsync(`node ${tempFile}`, { + timeout: 60000, // 60 second timeout (API calls can take time) + cwd: path.join(__dirname, '..') // Run from project root + }); + + // Check if stderr contains only warnings (not errors) + const hasRealErrors = stderr && !stderr.includes('[MODULE_TYPELESS_PACKAGE_JSON]'); + + return { + success: true, + output: stdout, + error: hasRealErrors ? stderr : null + }; + } catch (error) { + return { + success: false, + error: error.message, + stdout: error.stdout, + stderr: error.stderr, + code: error.code, + signal: error.signal, + killed: error.killed + }; + } finally { + // Clean up temp file + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } +} + +/** + * Test a curl command + */ +async function testCurlCommand(snippet) { + try { + // Extract and execute the curl command + const { stdout, stderr } = await execAsync(snippet.code, { + timeout: 10000, + shell: '/bin/bash' + }); + + // Try to parse JSON response + try { + const response = JSON.parse(stdout); + return { + success: true, + response, + rawOutput: stdout + }; + } catch (e) { + // Not JSON, but command succeeded + return { + success: true, + rawOutput: stdout, + error: stderr + }; + } + } catch (error) { + return { + success: false, + error: error.message, + stdout: error.stdout, + stderr: error.stderr + }; + } +} + +/** + * Test a bash command (npx, uvx, etc) + */ +async function testBashCommand(snippet) { + try { + // Execute the bash command - find the first non-comment, non-empty line + const lines = snippet.code.split('\n'); + const firstCommand = lines.find(line => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith('#'); + }); + + if (!firstCommand) { + return { success: false, error: 'No executable command found in bash snippet' }; + } + + // For multi-line commands with continuation, collect all continued lines + const commandParts = []; + const startIndex = lines.indexOf(firstCommand); + + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip empty lines and comments unless we're in a multi-line command + if (!line || line.startsWith('#')) { + if (commandParts.length === 0 || !commandParts[commandParts.length - 1].endsWith('\\')) { + if (commandParts.length > 0) break; // End of command + continue; // Skip to find start of command + } + } + + // Remove trailing backslash and add the line content + if (line.endsWith('\\')) { + commandParts.push(line.slice(0, -1).trim()); + } else { + commandParts.push(line); + break; // End of command + } + } + + const fullCommand = commandParts.join(' '); + + const { stdout, stderr } = await execAsync(fullCommand, { + timeout: 60000, // 60 second timeout for CLI commands + shell: '/bin/bash', + cwd: path.join(__dirname, '..') // Run from project root + }); + + return { + success: true, + output: stdout, + error: stderr + }; + } catch (error) { + return { + success: false, + error: error.message, + stdout: error.stdout, + stderr: error.stderr, + code: error.code, + signal: error.signal + }; + } +} + +/** + * Test a Python snippet + */ +async function testPythonSnippet(snippet) { + const tempFile = path.join(__dirname, `temp-snippet-${Date.now()}.py`); + + try { + // Write snippet to temporary file + fs.writeFileSync(tempFile, snippet.code); + + // Try uv environment first (if .venv exists), fallback to system python + const uvEnvExists = fs.existsSync(path.join(__dirname, '..', '.venv')); + const pythonCommand = uvEnvExists + ? `source .venv/bin/activate && python ${tempFile}` + : `python3 ${tempFile}`; + + // Execute from project root with activated environment + const { stdout, stderr } = await execAsync(pythonCommand, { + timeout: 60000, // 60 second timeout (API calls can take time) + cwd: path.join(__dirname, '..'), // Run from project root + shell: '/bin/bash' + }); + + return { + success: true, + output: stdout, + error: stderr + }; + } catch (error) { + return { + success: false, + error: error.message, + stdout: error.stdout, + stderr: error.stderr + }; + } finally { + // Clean up temp file + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } +} + +/** + * Validate a snippet based on its language + */ +async function validateSnippet(snippet) { + totalTests++; + + const relativePath = path.relative(DOCS_BASE_DIR, snippet.file); + const testName = `${relativePath}:${snippet.line} (${snippet.language} block #${snippet.index})`; + + log(`\nTesting: ${testName}`, 'info'); + log(` Code preview: ${snippet.code.substring(0, 60)}...`, 'dim'); + + if (!snippet.shouldTest) { + skippedTests++; + log(` ⊘ SKIPPED (not marked for testing)`, 'warning'); + return; + } + + let result; + + try { + switch (snippet.language.toLowerCase()) { + case 'javascript': + case 'typescript': + case 'js': + case 'ts': + result = await testJavaScriptSnippet(snippet); + break; + + case 'bash': + case 'sh': + case 'shell': + // Check if it's a supported bash command (skip comments to find actual command) + const lines = snippet.code.split('\n'); + const firstCommand = lines.find(line => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith('#'); + }); + + if (!firstCommand) { + result = { success: false, error: 'No executable command found in bash snippet' }; + } else { + // Extract the command name (first word) + const commandName = firstCommand.trim().split(/\s+/)[0]; + + // Skip informational commands (installation, navigation, etc.) + const SKIP_COMMANDS = ['npm', 'pip', 'pip3', 'cd', 'ls', 'mkdir', 'uv']; + if (SKIP_COMMANDS.includes(commandName)) { + skippedTests++; + log(` ⊘ SKIPPED (informational command: ${commandName})`, 'warning'); + return; + } + + // Test supported executable commands + if (commandName === 'curl') { + result = await testCurlCommand(snippet); + } else if (commandName === 'npx' || commandName === 'uvx') { + result = await testBashCommand(snippet); + } else { + result = { success: false, error: `Bash command '${commandName}' not supported for testing (only curl, npx, uvx)` }; + } + } + break; + + case 'python': + case 'py': + result = await testPythonSnippet(snippet); + break; + + default: + skippedTests++; + log(` ⊘ SKIPPED (language '${snippet.language}' not supported for testing)`, 'warning'); + return; + } + + if (result.success) { + passedTests++; + log(` βœ“ PASSED`, 'success'); + if (result.output) { + log(` Output: ${result.output.substring(0, 100)}...`, 'dim'); + } + } else { + failedTests++; + log(` βœ— FAILED`, 'error'); + log(` Error: ${result.error}`, 'error'); + if (result.code) log(` Exit code: ${result.code}`, 'error'); + if (result.signal) log(` Signal: ${result.signal}`, 'error'); + if (result.killed) log(` Killed: ${result.killed}`, 'error'); + if (result.stdout) { + log(` Stdout: ${result.stdout.substring(0, 200)}`, 'error'); + } + if (result.stderr) { + log(` Stderr: ${result.stderr.substring(0, 500)}`, 'error'); + } + } + } catch (error) { + failedTests++; + log(` βœ— FAILED (unexpected error)`, 'error'); + log(` ${error.message}`, 'error'); + } +} + +/** + * Main test runner + */ +async function runTests() { + log('=================================', 'info'); + log('Documentation Snippet Validation', 'info'); + log('=================================\n', 'info'); + + log(`Searching for documentation files in: ${DOCS_BASE_DIR}`, 'info'); + + const docFiles = findDocFiles(); + log(`Found ${docFiles.length} documentation files\n`, 'info'); + + // Extract all code blocks + const allSnippets = []; + for (const file of docFiles) { + const snippets = extractCodeBlocks(file); + allSnippets.push(...snippets); + } + + log(`Extracted ${allSnippets.length} code blocks total`, 'info'); + const testableSnippets = allSnippets.filter(s => s.shouldTest); + log(`Found ${testableSnippets.length} snippets marked for testing\n`, 'info'); + + // Run tests in parallel on testable snippets only (much faster!) + const CONCURRENCY = 5; // Run 5 tests at a time + const testableChunks = []; + for (let i = 0; i < testableSnippets.length; i += CONCURRENCY) { + testableChunks.push(testableSnippets.slice(i, i + CONCURRENCY)); + } + + for (const chunk of testableChunks) { + await Promise.all(chunk.map(snippet => validateSnippet(snippet))); + } + + // Also process non-testable snippets (just to count them as skipped) + const nonTestableSnippets = allSnippets.filter(s => !s.shouldTest); + for (const snippet of nonTestableSnippets) { + totalTests++; + skippedTests++; + } + + // Print summary + log('\n=================================', 'info'); + log('Test Summary', 'info'); + log('=================================', 'info'); + log(`Total snippets found: ${allSnippets.length}`, 'info'); + log(`Tests run: ${totalTests}`, 'info'); + log(`Passed: ${passedTests}`, 'success'); + log(`Failed: ${failedTests}`, failedTests > 0 ? 'error' : 'info'); + log(`Skipped: ${skippedTests}`, 'warning'); + + // Exit with error code if any tests failed + if (failedTests > 0) { + log('\n❌ Some snippet tests failed', 'error'); + process.exit(1); + } else if (passedTests === 0 && testableSnippets.length === 0) { + log('\n⚠️ No testable snippets found. Mark snippets with "test=true" to enable testing.', 'warning'); + log(' Example: ```javascript test=true', 'dim'); + process.exit(0); + } else { + log('\nβœ… All snippet tests passed!', 'success'); + process.exit(0); + } +} + +// Run tests +runTests().catch(error => { + log(`\nFatal error: ${error.message}`, 'error'); + console.error(error); + process.exit(1); +}); diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..09dc3c59 --- /dev/null +++ b/uv.lock @@ -0,0 +1,933 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "a2a-sdk" +version = "0.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/2c/6eff205080a4fb3937745f0bab4ff58716cdcc524acd077a493612d34336/a2a_sdk-0.3.11.tar.gz", hash = "sha256:194a6184d3e5c1c5d8941eb64fb33c346df3ebbec754effed8403f253bedb085", size = 226923, upload-time = "2025-11-07T11:05:38.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/f9/3e633485a3f23f5b3e04a7f0d3e690ae918fd1252941e8107c7593d882f1/a2a_sdk-0.3.11-py3-none-any.whl", hash = "sha256:f57673d5f38b3e0eb7c5b57e7dc126404d02c54c90692395ab4fd06aaa80cc8f", size = 140381, upload-time = "2025-11-07T11:05:37.093Z" }, +] + +[[package]] +name = "adcp" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "a2a-sdk" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/19/993dbb46473e79ee6866862fa65ad586b8581a97bfc819190a8f02ed9f11/adcp-1.4.0.tar.gz", hash = "sha256:b0f4785488182b018c7bb16bf319da59b8f61ffd713970a2c1fe7d6b563ab5ee", size = 84080, upload-time = "2025-11-10T15:13:08.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/bb/7c93812aadd7253ac6d05fbe8cd056fe230f7025265b4496c1d102db4d55/adcp-1.4.0-py3-none-any.whl", hash = "sha256:fe1a09bf2427c688217f8848e0ac4a44f5845ecf926503391e107c49afc755d9", size = 67894, upload-time = "2025-11-10T15:13:07.031Z" }, +] + +[[package]] +name = "adcp-docs" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "adcp" }, +] + +[package.metadata] +requires-dist = [{ name = "adcp", specifier = ">=1.4.0" }] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, +] + +[[package]] +name = "google-auth" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "mcp" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/54/dd2330ef4611c27ae59124820863c34e1d3edb1133c58e6375e2d938c9c5/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855", size = 452697, upload-time = "2025-11-06T23:19:58.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/850b6edc96c03bd44b00de9a0ca3c1cc71e0ba1cd5822955bc9e4eb3fad3/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b", size = 173672, upload-time = "2025-11-06T23:19:56.508Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" }, + { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" }, + { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" }, + { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, + { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, + { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +]