adcp 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import logging
7
- from typing import Any, TypeVar
7
+ from typing import Any, TypeVar, Union, cast, get_args, get_origin
8
8
 
9
9
  from pydantic import BaseModel, ValidationError
10
10
 
@@ -13,6 +13,56 @@ logger = logging.getLogger(__name__)
13
13
  T = TypeVar("T", bound=BaseModel)
14
14
 
15
15
 
16
+ def _validate_union_type(data: dict[str, Any], response_type: type[T]) -> T:
17
+ """
18
+ Validate data against a Union type by trying each variant.
19
+
20
+ Args:
21
+ data: Data to validate
22
+ response_type: Union type to validate against
23
+
24
+ Returns:
25
+ Validated model instance
26
+
27
+ Raises:
28
+ ValidationError: If data doesn't match any Union variant
29
+ """
30
+ # Check if this is a Union type (handles both typing.Union and types.UnionType)
31
+ origin = get_origin(response_type)
32
+
33
+ # In Python 3.10+, X | Y creates a types.UnionType, not typing.Union
34
+ # We need to check both the origin and the type itself
35
+ is_union = origin is Union or str(type(response_type).__name__) == "UnionType"
36
+
37
+ if is_union:
38
+ # Get union args - works for both typing.Union and types.UnionType
39
+ args = get_args(response_type)
40
+ if not args: # types.UnionType case
41
+ # For types.UnionType, we need to access __args__ directly
42
+ args = getattr(response_type, "__args__", ())
43
+
44
+ errors = []
45
+ for variant in args:
46
+ try:
47
+ return cast(T, variant.model_validate(data))
48
+ except ValidationError as e:
49
+ errors.append((variant.__name__, e))
50
+ continue
51
+
52
+ # If we get here, none of the variants worked
53
+ error_msgs = [f"{name}: {str(e)}" for name, e in errors]
54
+ # Raise a ValueError instead of ValidationError for better error messages
55
+ raise ValueError(
56
+ f"Data doesn't match any Union variant. "
57
+ f"Attempted variants: {', '.join([e[0] for e in errors])}. "
58
+ f"Errors: {'; '.join(error_msgs)}"
59
+ )
60
+
61
+ # Not a Union type, use regular validation
62
+ # Cast is needed because response_type is typed as type[T] | Any
63
+ return cast(T, response_type.model_validate(data)) # type: ignore[redundant-cast]
64
+
65
+
16
66
  def parse_mcp_content(content: list[dict[str, Any]], response_type: type[T]) -> T:
17
67
  """
18
68
  Parse MCP content array into structured response type.
@@ -48,8 +98,8 @@ def parse_mcp_content(content: list[dict[str, Any]], response_type: type[T]) ->
48
98
  try:
49
99
  # Try parsing as JSON
50
100
  data = json.loads(text)
51
- # Validate against expected schema
52
- return response_type.model_validate(data)
101
+ # Validate against expected schema (handles Union types)
102
+ return _validate_union_type(data, response_type)
53
103
  except json.JSONDecodeError:
54
104
  # Not JSON, try next item
55
105
  continue
@@ -61,7 +111,7 @@ def parse_mcp_content(content: list[dict[str, Any]], response_type: type[T]) ->
61
111
  elif item.get("type") == "resource":
62
112
  # Resource content might have structured data
63
113
  try:
64
- return response_type.model_validate(item)
114
+ return _validate_union_type(item, response_type)
65
115
  except ValidationError:
66
116
  # Try next item
67
117
  continue
@@ -98,25 +148,24 @@ def parse_json_or_text(data: Any, response_type: type[T]) -> T:
98
148
  # If already a dict, try direct validation
99
149
  if isinstance(data, dict):
100
150
  try:
101
- return response_type.model_validate(data)
151
+ return _validate_union_type(data, response_type)
102
152
  except ValidationError as e:
103
- raise ValueError(
104
- f"Response doesn't match expected schema {response_type.__name__}: {e}"
105
- ) from e
153
+ # Get the type name, handling Union types
154
+ type_name = getattr(response_type, "__name__", str(response_type))
155
+ raise ValueError(f"Response doesn't match expected schema {type_name}: {e}") from e
106
156
 
107
157
  # If string, try JSON parsing
108
158
  if isinstance(data, str):
109
159
  try:
110
160
  parsed = json.loads(data)
111
- return response_type.model_validate(parsed)
161
+ return _validate_union_type(parsed, response_type)
112
162
  except json.JSONDecodeError as e:
113
163
  raise ValueError(f"Response is not valid JSON: {e}") from e
114
164
  except ValidationError as e:
115
- raise ValueError(
116
- f"Response doesn't match expected schema {response_type.__name__}: {e}"
117
- ) from e
165
+ # Get the type name, handling Union types
166
+ type_name = getattr(response_type, "__name__", str(response_type))
167
+ raise ValueError(f"Response doesn't match expected schema {type_name}: {e}") from e
118
168
 
119
169
  # Unsupported type
120
- raise ValueError(
121
- f"Cannot parse response of type {type(data).__name__} into {response_type.__name__}"
122
- )
170
+ type_name = getattr(response_type, "__name__", str(response_type))
171
+ raise ValueError(f"Cannot parse response of type {type(data).__name__} into {type_name}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Official Python client for the Ad Context Protocol (AdCP)
5
5
  Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
6
  License: Apache-2.0
@@ -59,8 +59,45 @@ AdCP operations are **distributed and asynchronous by default**. An agent might:
59
59
  pip install adcp
60
60
  ```
61
61
 
62
+ > **Note**: This client requires Python 3.10 or later and supports both synchronous and asynchronous workflows.
63
+
64
+ ## Quick Start: Test Helpers
65
+
66
+ The fastest way to get started is using the pre-configured test agents:
67
+
68
+ ```python
69
+ from adcp.testing import test_agent
70
+ from adcp.types.generated import GetProductsRequest
71
+
72
+ # Zero configuration - just import and use!
73
+ result = await test_agent.get_products(
74
+ GetProductsRequest(
75
+ brief="Coffee subscription service",
76
+ promoted_offering="Premium coffee deliveries"
77
+ )
78
+ )
79
+
80
+ if result.success:
81
+ print(f"Found {len(result.data.products)} products")
82
+ ```
83
+
84
+ Test helpers include:
85
+ - **`test_agent`**: Pre-configured MCP test agent with authentication
86
+ - **`test_agent_a2a`**: Pre-configured A2A test agent with authentication
87
+ - **`test_agent_no_auth`**: Pre-configured MCP test agent WITHOUT authentication
88
+ - **`test_agent_a2a_no_auth`**: Pre-configured A2A test agent WITHOUT authentication
89
+ - **`creative_agent`**: Reference creative agent for preview functionality
90
+ - **`test_agent_client`**: Multi-agent client with both protocols
91
+ - **`create_test_agent()`**: Factory for custom test configurations
92
+
93
+ > **Note**: Test agents are rate-limited and for testing/examples only. DO NOT use in production.
94
+
95
+ See [examples/test_helpers_demo.py](examples/test_helpers_demo.py) for more examples.
96
+
62
97
  ## Quick Start: Distributed Operations
63
98
 
99
+ For production use, configure your own agents:
100
+
64
101
  ```python
65
102
  from adcp import ADCPMultiAgentClient, AgentConfig, GetProductsRequest
66
103
 
@@ -110,6 +147,66 @@ async with ADCPMultiAgentClient(
110
147
 
111
148
  ## Features
112
149
 
150
+ ### Test Helpers
151
+
152
+ Pre-configured test agents for instant prototyping and testing:
153
+
154
+ ```python
155
+ from adcp.testing import (
156
+ test_agent, test_agent_a2a,
157
+ test_agent_no_auth, test_agent_a2a_no_auth,
158
+ creative_agent, test_agent_client, create_test_agent
159
+ )
160
+ from adcp.types.generated import GetProductsRequest, PreviewCreativeRequest
161
+
162
+ # 1. Single agent with authentication (MCP)
163
+ result = await test_agent.get_products(
164
+ GetProductsRequest(brief="Coffee brands")
165
+ )
166
+
167
+ # 2. Single agent with authentication (A2A)
168
+ result = await test_agent_a2a.get_products(
169
+ GetProductsRequest(brief="Coffee brands")
170
+ )
171
+
172
+ # 3. Single agent WITHOUT authentication (MCP)
173
+ # Useful for testing unauthenticated behavior
174
+ result = await test_agent_no_auth.get_products(
175
+ GetProductsRequest(brief="Coffee brands")
176
+ )
177
+
178
+ # 4. Single agent WITHOUT authentication (A2A)
179
+ result = await test_agent_a2a_no_auth.get_products(
180
+ GetProductsRequest(brief="Coffee brands")
181
+ )
182
+
183
+ # 5. Creative agent (preview functionality, no auth required)
184
+ result = await creative_agent.preview_creative(
185
+ PreviewCreativeRequest(
186
+ manifest={"format_id": "banner_300x250", "assets": {...}}
187
+ )
188
+ )
189
+
190
+ # 6. Multi-agent (parallel execution with both protocols)
191
+ results = await test_agent_client.get_products(
192
+ GetProductsRequest(brief="Coffee brands")
193
+ )
194
+
195
+ # 7. Custom configuration
196
+ from adcp.client import ADCPClient
197
+ config = create_test_agent(id="my-test", timeout=60.0)
198
+ client = ADCPClient(config)
199
+ ```
200
+
201
+ **Use cases:**
202
+ - Quick prototyping and experimentation
203
+ - Example code and documentation
204
+ - Integration testing without mock servers
205
+ - Testing authentication behavior (comparing auth vs no-auth results)
206
+ - Learning AdCP concepts
207
+
208
+ **Important:** Test agents are public, rate-limited, and for testing only. Never use in production.
209
+
113
210
  ### Full Protocol Support
114
211
  - **A2A Protocol**: Native support for Agent-to-Agent protocol
115
212
  - **MCP Protocol**: Native support for Model Context Protocol
@@ -375,6 +472,46 @@ uvx adcp --json myagent get_products '{"brief":"TV ads"}'
375
472
  uvx adcp --debug myagent get_products '{"brief":"TV ads"}'
376
473
  ```
377
474
 
475
+ ### Using Test Agents from CLI
476
+
477
+ The CLI provides easy access to public test agents without configuration:
478
+
479
+ ```bash
480
+ # Use test agent with authentication (MCP)
481
+ uvx adcp https://test-agent.adcontextprotocol.org/mcp/ \
482
+ --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ \
483
+ get_products '{"brief":"Coffee brands"}'
484
+
485
+ # Use test agent WITHOUT authentication (MCP)
486
+ uvx adcp https://test-agent.adcontextprotocol.org/mcp/ \
487
+ get_products '{"brief":"Coffee brands"}'
488
+
489
+ # Use test agent with authentication (A2A)
490
+ uvx adcp --protocol a2a \
491
+ --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ \
492
+ https://test-agent.adcontextprotocol.org \
493
+ get_products '{"brief":"Coffee brands"}'
494
+
495
+ # Save test agent for easier access
496
+ uvx adcp --save-auth test-agent https://test-agent.adcontextprotocol.org/mcp/ mcp
497
+ # Enter token when prompted: 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ
498
+
499
+ # Now use saved config
500
+ uvx adcp test-agent get_products '{"brief":"Coffee brands"}'
501
+
502
+ # Use creative agent (no auth required)
503
+ uvx adcp https://creative.adcontextprotocol.org/mcp \
504
+ preview_creative @creative_manifest.json
505
+ ```
506
+
507
+ **Test Agent Details:**
508
+ - **URL (MCP)**: `https://test-agent.adcontextprotocol.org/mcp/`
509
+ - **URL (A2A)**: `https://test-agent.adcontextprotocol.org`
510
+ - **Auth Token**: `1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ` (optional, public token)
511
+ - **Rate Limited**: For testing only, not for production
512
+ - **No Auth Mode**: Omit `--auth` flag to test unauthenticated behavior
513
+ ```
514
+
378
515
  ### Configuration Management
379
516
 
380
517
  ```bash
@@ -1,23 +1,25 @@
1
- adcp/__init__.py,sha256=PKqv9xsX2JUnDqOIvOGsfYFYQX4QlWKOt5w0R4IiRXw,2512
1
+ adcp/__init__.py,sha256=FRJ5kL682ZQiohuDyzUzwLvrOEUM7Hjy0aavfo4g4gc,6724
2
2
  adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
3
- adcp/client.py,sha256=xs_sG7soRH1szk0S0rFu_6Ge4Ffe2aUdaTYnLmvteeo,27950
3
+ adcp/client.py,sha256=co4tKdkjDrqZltCUPcYwVRr8LQfbBbG-DLjapLjcVuo,28246
4
4
  adcp/config.py,sha256=Vsy7ZPOI8G3fB_i5Nk-CHbC7wdasCUWuKlos0fwA0kY,2017
5
5
  adcp/exceptions.py,sha256=dNRMKV23DlkGKyB9Xmt6MtlhvDu1crjzD_en4nAEwDY,4399
6
6
  adcp/protocols/__init__.py,sha256=6UFwACQ0QadBUzy17wUROHqsJDp8ztPW2jzyl53Zh_g,262
7
7
  adcp/protocols/a2a.py,sha256=FHgc6G_eU2qD0vH7_RyS1eZvUFSb2j3-EsceoHPi384,12467
8
- adcp/protocols/base.py,sha256=CGqUilQv_ymhnfdowBV_HJhIxYUDM3sRO7ahW-kRB0M,5087
8
+ adcp/protocols/base.py,sha256=vBHD23Fzl_CCk_Gy9nvSbBYopcJlYkYyzoz-rhI8wHg,5214
9
9
  adcp/protocols/mcp.py,sha256=eIk8snCinZm-ZjdarGVMt5nEYJ4_8POM9Fa5Mkw7xxU,15902
10
+ adcp/testing/__init__.py,sha256=kPsRncZ42g4HpOljVwLrFXm2O28K5NKcPUtvykfYY6M,888
11
+ adcp/testing/test_helpers.py,sha256=4n8fZYy1cVpjZpFW2SxBzpC8fmY-MBFrzY4tIPqe4rQ,10028
10
12
  adcp/types/__init__.py,sha256=3E_TJUXqQQFcjmSZZSPLwqBP3s_ijsH2LDeuOU-MP30,402
11
13
  adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
12
- adcp/types/generated.py,sha256=UmHVH22lBayrNipgctAE-K_nsUuRfXvmDNhKZVq9mxQ,56514
14
+ adcp/types/generated.py,sha256=Ig4ucbJzKRuHlwYzsqvMF9M3w2KghhQQqsXuOnBqVMM,74993
13
15
  adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
14
16
  adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
15
17
  adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
16
18
  adcp/utils/preview_cache.py,sha256=8_2qs5CgrHv1_WOnD4bs43VWueu-rcZRu5PZMQ_lyuE,17573
17
- adcp/utils/response_parser.py,sha256=NQTLlbvmnM_tE4B5w3oB1Wshny1p-Uh8IWbghlwoNJc,4057
18
- adcp-1.2.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
19
- adcp-1.2.0.dist-info/METADATA,sha256=NJowqCc7uoc4hx99dhdokdnydbZcnl9ERjyEsY7-q2o,14455
20
- adcp-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- adcp-1.2.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
22
- adcp-1.2.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
23
- adcp-1.2.0.dist-info/RECORD,,
19
+ adcp/utils/response_parser.py,sha256=uPk2vIH-RYZmq7y3i8lC4HTMQ3FfKdlgXKTjgJ1955M,6253
20
+ adcp-1.3.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
21
+ adcp-1.3.0.dist-info/METADATA,sha256=QQQ8zxzwIelNWUxz-ZBTTgpORHp9RrxT-1BnjFZ5S_o,19097
22
+ adcp-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ adcp-1.3.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
24
+ adcp-1.3.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
25
+ adcp-1.3.0.dist-info/RECORD,,
File without changes