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.
- adcp/__init__.py +210 -17
- adcp/client.py +12 -5
- adcp/protocols/base.py +5 -2
- adcp/testing/__init__.py +38 -0
- adcp/testing/test_helpers.py +311 -0
- adcp/types/generated.py +464 -55
- adcp/utils/response_parser.py +64 -15
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/METADATA +138 -1
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/RECORD +13 -11
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/WHEEL +0 -0
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/entry_points.txt +0 -0
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {adcp-1.2.0.dist-info → adcp-1.3.0.dist-info}/top_level.txt +0 -0
adcp/utils/response_parser.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
151
|
+
return _validate_union_type(data, response_type)
|
|
102
152
|
except ValidationError as e:
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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.
|
|
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=
|
|
1
|
+
adcp/__init__.py,sha256=FRJ5kL682ZQiohuDyzUzwLvrOEUM7Hjy0aavfo4g4gc,6724
|
|
2
2
|
adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
|
|
3
|
-
adcp/client.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
18
|
-
adcp-1.
|
|
19
|
-
adcp-1.
|
|
20
|
-
adcp-1.
|
|
21
|
-
adcp-1.
|
|
22
|
-
adcp-1.
|
|
23
|
-
adcp-1.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|