adcp 1.3.1__tar.gz → 1.4.1__tar.gz

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.
Files changed (41) hide show
  1. {adcp-1.3.1/src/adcp.egg-info → adcp-1.4.1}/PKG-INFO +46 -18
  2. {adcp-1.3.1 → adcp-1.4.1}/README.md +45 -17
  3. {adcp-1.3.1 → adcp-1.4.1}/pyproject.toml +1 -1
  4. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/__init__.py +1 -1
  5. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/client.py +5 -0
  6. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/exceptions.py +34 -0
  7. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/protocols/mcp.py +38 -16
  8. adcp-1.4.1/src/adcp/simple.py +347 -0
  9. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/testing/__init__.py +15 -0
  10. {adcp-1.3.1 → adcp-1.4.1/src/adcp.egg-info}/PKG-INFO +46 -18
  11. {adcp-1.3.1 → adcp-1.4.1}/src/adcp.egg-info/SOURCES.txt +3 -1
  12. {adcp-1.3.1 → adcp-1.4.1}/tests/test_protocols.py +27 -3
  13. adcp-1.4.1/tests/test_simple_api.py +183 -0
  14. {adcp-1.3.1 → adcp-1.4.1}/LICENSE +0 -0
  15. {adcp-1.3.1 → adcp-1.4.1}/setup.cfg +0 -0
  16. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/__main__.py +0 -0
  17. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/config.py +0 -0
  18. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/protocols/__init__.py +0 -0
  19. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/protocols/a2a.py +0 -0
  20. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/protocols/base.py +0 -0
  21. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/testing/test_helpers.py +0 -0
  22. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/types/__init__.py +0 -0
  23. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/types/core.py +0 -0
  24. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/types/generated.py +0 -0
  25. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/types/tasks.py +0 -0
  26. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/utils/__init__.py +0 -0
  27. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/utils/operation_id.py +0 -0
  28. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/utils/preview_cache.py +0 -0
  29. {adcp-1.3.1 → adcp-1.4.1}/src/adcp/utils/response_parser.py +0 -0
  30. {adcp-1.3.1 → adcp-1.4.1}/src/adcp.egg-info/dependency_links.txt +0 -0
  31. {adcp-1.3.1 → adcp-1.4.1}/src/adcp.egg-info/entry_points.txt +0 -0
  32. {adcp-1.3.1 → adcp-1.4.1}/src/adcp.egg-info/requires.txt +0 -0
  33. {adcp-1.3.1 → adcp-1.4.1}/src/adcp.egg-info/top_level.txt +0 -0
  34. {adcp-1.3.1 → adcp-1.4.1}/tests/test_cli.py +0 -0
  35. {adcp-1.3.1 → adcp-1.4.1}/tests/test_client.py +0 -0
  36. {adcp-1.3.1 → adcp-1.4.1}/tests/test_code_generation.py +0 -0
  37. {adcp-1.3.1 → adcp-1.4.1}/tests/test_discriminated_unions.py +0 -0
  38. {adcp-1.3.1 → adcp-1.4.1}/tests/test_format_id_validation.py +0 -0
  39. {adcp-1.3.1 → adcp-1.4.1}/tests/test_helpers.py +0 -0
  40. {adcp-1.3.1 → adcp-1.4.1}/tests/test_preview_html.py +0 -0
  41. {adcp-1.3.1 → adcp-1.4.1}/tests/test_response_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 1.3.1
3
+ Version: 1.4.1
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
@@ -63,36 +63,64 @@ pip install adcp
63
63
 
64
64
  ## Quick Start: Test Helpers
65
65
 
66
- The fastest way to get started is using the pre-configured test agents:
66
+ The fastest way to get started is using pre-configured test agents with the **`.simple` API**:
67
67
 
68
68
  ```python
69
69
  from adcp.testing import test_agent
70
- from adcp.types.generated import GetProductsRequest
71
70
 
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
- )
71
+ # Zero configuration - just import and call with kwargs!
72
+ products = await test_agent.simple.get_products(
73
+ brief='Coffee subscription service for busy professionals'
78
74
  )
79
75
 
80
- if result.success:
81
- print(f"Found {len(result.data.products)} products")
76
+ print(f"Found {len(products.products)} products")
77
+ ```
78
+
79
+ ### Simple vs. Standard API
80
+
81
+ **Every ADCPClient** includes both API styles via the `.simple` accessor:
82
+
83
+ **Simple API** (`client.simple.*`) - Recommended for examples/prototyping:
84
+ ```python
85
+ from adcp.testing import test_agent
86
+
87
+ # Kwargs and direct return - raises on error
88
+ products = await test_agent.simple.get_products(brief='Coffee brands')
89
+ print(products.products[0].name)
90
+ ```
91
+
92
+ **Standard API** (`client.*`) - Recommended for production:
93
+ ```python
94
+ from adcp.testing import test_agent
95
+ from adcp.types.generated import GetProductsRequest
96
+
97
+ # Explicit request objects and TaskResult wrapper
98
+ request = GetProductsRequest(brief='Coffee brands')
99
+ result = await test_agent.get_products(request)
100
+
101
+ if result.success and result.data:
102
+ print(result.data.products[0].name)
103
+ else:
104
+ print(f"Error: {result.error}")
82
105
  ```
83
106
 
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
107
+ **When to use which:**
108
+ - **Simple API** (`.simple`): Quick testing, documentation, examples, notebooks
109
+ - **Standard API**: Production code, complex error handling, webhook workflows
110
+
111
+ ### Available Test Helpers
112
+
113
+ Pre-configured agents (all include `.simple` accessor):
114
+ - **`test_agent`**: MCP test agent with authentication
115
+ - **`test_agent_a2a`**: A2A test agent with authentication
116
+ - **`test_agent_no_auth`**: MCP test agent without authentication
117
+ - **`test_agent_a2a_no_auth`**: A2A test agent without authentication
89
118
  - **`creative_agent`**: Reference creative agent for preview functionality
90
119
  - **`test_agent_client`**: Multi-agent client with both protocols
91
- - **`create_test_agent()`**: Factory for custom test configurations
92
120
 
93
121
  > **Note**: Test agents are rate-limited and for testing/examples only. DO NOT use in production.
94
122
 
95
- See [examples/test_helpers_demo.py](examples/test_helpers_demo.py) for more examples.
123
+ See [examples/simple_api_demo.py](examples/simple_api_demo.py) for a complete comparison.
96
124
 
97
125
  ## Quick Start: Distributed Operations
98
126
 
@@ -26,36 +26,64 @@ pip install adcp
26
26
 
27
27
  ## Quick Start: Test Helpers
28
28
 
29
- The fastest way to get started is using the pre-configured test agents:
29
+ The fastest way to get started is using pre-configured test agents with the **`.simple` API**:
30
30
 
31
31
  ```python
32
32
  from adcp.testing import test_agent
33
- from adcp.types.generated import GetProductsRequest
34
33
 
35
- # Zero configuration - just import and use!
36
- result = await test_agent.get_products(
37
- GetProductsRequest(
38
- brief="Coffee subscription service",
39
- promoted_offering="Premium coffee deliveries"
40
- )
34
+ # Zero configuration - just import and call with kwargs!
35
+ products = await test_agent.simple.get_products(
36
+ brief='Coffee subscription service for busy professionals'
41
37
  )
42
38
 
43
- if result.success:
44
- print(f"Found {len(result.data.products)} products")
39
+ print(f"Found {len(products.products)} products")
40
+ ```
41
+
42
+ ### Simple vs. Standard API
43
+
44
+ **Every ADCPClient** includes both API styles via the `.simple` accessor:
45
+
46
+ **Simple API** (`client.simple.*`) - Recommended for examples/prototyping:
47
+ ```python
48
+ from adcp.testing import test_agent
49
+
50
+ # Kwargs and direct return - raises on error
51
+ products = await test_agent.simple.get_products(brief='Coffee brands')
52
+ print(products.products[0].name)
53
+ ```
54
+
55
+ **Standard API** (`client.*`) - Recommended for production:
56
+ ```python
57
+ from adcp.testing import test_agent
58
+ from adcp.types.generated import GetProductsRequest
59
+
60
+ # Explicit request objects and TaskResult wrapper
61
+ request = GetProductsRequest(brief='Coffee brands')
62
+ result = await test_agent.get_products(request)
63
+
64
+ if result.success and result.data:
65
+ print(result.data.products[0].name)
66
+ else:
67
+ print(f"Error: {result.error}")
45
68
  ```
46
69
 
47
- Test helpers include:
48
- - **`test_agent`**: Pre-configured MCP test agent with authentication
49
- - **`test_agent_a2a`**: Pre-configured A2A test agent with authentication
50
- - **`test_agent_no_auth`**: Pre-configured MCP test agent WITHOUT authentication
51
- - **`test_agent_a2a_no_auth`**: Pre-configured A2A test agent WITHOUT authentication
70
+ **When to use which:**
71
+ - **Simple API** (`.simple`): Quick testing, documentation, examples, notebooks
72
+ - **Standard API**: Production code, complex error handling, webhook workflows
73
+
74
+ ### Available Test Helpers
75
+
76
+ Pre-configured agents (all include `.simple` accessor):
77
+ - **`test_agent`**: MCP test agent with authentication
78
+ - **`test_agent_a2a`**: A2A test agent with authentication
79
+ - **`test_agent_no_auth`**: MCP test agent without authentication
80
+ - **`test_agent_a2a_no_auth`**: A2A test agent without authentication
52
81
  - **`creative_agent`**: Reference creative agent for preview functionality
53
82
  - **`test_agent_client`**: Multi-agent client with both protocols
54
- - **`create_test_agent()`**: Factory for custom test configurations
55
83
 
56
84
  > **Note**: Test agents are rate-limited and for testing/examples only. DO NOT use in production.
57
85
 
58
- See [examples/test_helpers_demo.py](examples/test_helpers_demo.py) for more examples.
86
+ See [examples/simple_api_demo.py](examples/simple_api_demo.py) for a complete comparison.
59
87
 
60
88
  ## Quick Start: Distributed Operations
61
89
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "adcp"
7
- version = "1.3.1"
7
+ version = "1.4.1"
8
8
  description = "Official Python client for the Ad Context Protocol (AdCP)"
9
9
  authors = [
10
10
  {name = "AdCP Community", email = "maintainers@adcontextprotocol.org"}
@@ -150,7 +150,7 @@ from adcp.types.generated import (
150
150
  TaskStatus as GeneratedTaskStatus,
151
151
  )
152
152
 
153
- __version__ = "1.3.1"
153
+ __version__ = "1.4.1"
154
154
 
155
155
  __all__ = [
156
156
  # Client classes
@@ -86,6 +86,11 @@ class ADCPClient:
86
86
  else:
87
87
  raise ValueError(f"Unsupported protocol: {agent_config.protocol}")
88
88
 
89
+ # Initialize simple API accessor (lazy import to avoid circular dependency)
90
+ from adcp.simple import SimpleAPI
91
+
92
+ self.simple = SimpleAPI(self)
93
+
89
94
  def get_webhook_url(self, task_type: str, operation_id: str) -> str:
90
95
  """Generate webhook URL for a task."""
91
96
  if not self.webhook_url_template:
@@ -119,3 +119,37 @@ class ADCPWebhookSignatureError(ADCPWebhookError):
119
119
  " Webhook signatures use HMAC-SHA256 for security."
120
120
  )
121
121
  super().__init__(message, agent_id, None, suggestion)
122
+
123
+
124
+ class ADCPSimpleAPIError(ADCPError):
125
+ """Error from simplified API (.simple accessor).
126
+
127
+ Raised when a simple API method fails. The underlying error details
128
+ are available in the message. For more control over error handling,
129
+ use the standard API (client.method()) instead of client.simple.method().
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ operation: str,
135
+ error_message: str | None = None,
136
+ agent_id: str | None = None,
137
+ ):
138
+ """Initialize simple API error.
139
+
140
+ Args:
141
+ operation: The operation that failed (e.g., "get_products")
142
+ error_message: The underlying error message from TaskResult
143
+ agent_id: Optional agent ID for context
144
+ """
145
+ message = f"{operation} failed"
146
+ if error_message:
147
+ message = f"{message}: {error_message}"
148
+
149
+ suggestion = (
150
+ f"For more control over error handling, use the standard API:\n"
151
+ f" result = await client.{operation}(request)\n"
152
+ f" if not result.success:\n"
153
+ f" # Handle error with full TaskResult context"
154
+ )
155
+ super().__init__(message, agent_id, None, suggestion)
@@ -245,24 +245,12 @@ class MCPAdapter(ProtocolAdapter):
245
245
  # Call the tool using MCP client session
246
246
  result = await session.call_tool(tool_name, params)
247
247
 
248
- # This SDK requires MCP tools to return structuredContent
249
- # The content field may contain human-readable messages but the actual
250
- # response data must be in structuredContent
251
- if not hasattr(result, "structuredContent") or result.structuredContent is None:
252
- raise ValueError(
253
- f"MCP tool {tool_name} did not return structuredContent. "
254
- f"This SDK requires MCP tools to provide structured responses. "
255
- f"Got content: {result.content if hasattr(result, 'content') else 'none'}"
256
- )
248
+ # Check if this is an error response
249
+ is_error = hasattr(result, "isError") and result.isError
257
250
 
258
- # Extract the structured data (required)
259
- data_to_return = result.structuredContent
260
-
261
- # Extract human-readable message from content (optional)
262
- # This is typically a status message like "Found 42 creative formats"
251
+ # Extract human-readable message from content
263
252
  message_text = None
264
253
  if hasattr(result, "content") and result.content:
265
- # Serialize content using the same method used for backward compatibility
266
254
  serialized_content = self._serialize_mcp_content(result.content)
267
255
  if isinstance(serialized_content, list):
268
256
  for item in serialized_content:
@@ -271,6 +259,40 @@ class MCPAdapter(ProtocolAdapter):
271
259
  message_text = item["text"]
272
260
  break
273
261
 
262
+ # Handle error responses
263
+ if is_error:
264
+ # For error responses, structuredContent is optional
265
+ # Use the error message from content as the error
266
+ error_message = message_text or "Tool execution failed"
267
+ if self.agent_config.debug and start_time:
268
+ duration_ms = (time.time() - start_time) * 1000
269
+ debug_info = DebugInfo(
270
+ request=debug_request,
271
+ response={
272
+ "error": error_message,
273
+ "is_error": True,
274
+ },
275
+ duration_ms=duration_ms,
276
+ )
277
+ return TaskResult[Any](
278
+ status=TaskStatus.FAILED,
279
+ error=error_message,
280
+ success=False,
281
+ debug_info=debug_info,
282
+ )
283
+
284
+ # For successful responses, structuredContent is required
285
+ if not hasattr(result, "structuredContent") or result.structuredContent is None:
286
+ raise ValueError(
287
+ f"MCP tool {tool_name} did not return structuredContent. "
288
+ f"This SDK requires MCP tools to provide structured responses "
289
+ f"for successful calls. "
290
+ f"Got content: {result.content if hasattr(result, 'content') else 'none'}"
291
+ )
292
+
293
+ # Extract the structured data (required for success)
294
+ data_to_return = result.structuredContent
295
+
274
296
  if self.agent_config.debug and start_time:
275
297
  duration_ms = (time.time() - start_time) * 1000
276
298
  debug_info = DebugInfo(
@@ -278,7 +300,7 @@ class MCPAdapter(ProtocolAdapter):
278
300
  response={
279
301
  "data": data_to_return,
280
302
  "message": message_text,
281
- "is_error": result.isError if hasattr(result, "isError") else False,
303
+ "is_error": False,
282
304
  },
283
305
  duration_ms=duration_ms,
284
306
  )
@@ -0,0 +1,347 @@
1
+ """Simplified API accessor for ADCPClient.
2
+
3
+ Provides an ergonomic API with:
4
+ - Kwargs instead of request objects
5
+ - Direct return values (no TaskResult unwrapping)
6
+ - Raises exceptions on errors
7
+
8
+ Usage:
9
+ client = ADCPClient(config)
10
+
11
+ # Standard API: full control
12
+ result = await client.get_products(GetProductsRequest(brief="Coffee"))
13
+ if result.success:
14
+ print(result.data.products)
15
+
16
+ # Simple API: ergonomic
17
+ products = await client.simple.get_products(brief="Coffee")
18
+ print(products.products)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from adcp.exceptions import ADCPSimpleAPIError
26
+ from adcp.types.generated import (
27
+ ActivateSignalRequest,
28
+ ActivateSignalResponse,
29
+ GetMediaBuyDeliveryRequest,
30
+ GetMediaBuyDeliveryResponse,
31
+ GetProductsRequest,
32
+ GetProductsResponse,
33
+ GetSignalsRequest,
34
+ GetSignalsResponse,
35
+ ListAuthorizedPropertiesRequest,
36
+ ListAuthorizedPropertiesResponse,
37
+ ListCreativeFormatsRequest,
38
+ ListCreativeFormatsResponse,
39
+ ListCreativesRequest,
40
+ ListCreativesResponse,
41
+ PreviewCreativeRequest,
42
+ PreviewCreativeResponse,
43
+ ProvidePerformanceFeedbackRequest,
44
+ ProvidePerformanceFeedbackResponse,
45
+ SyncCreativesRequest,
46
+ SyncCreativesResponse,
47
+ )
48
+
49
+ if TYPE_CHECKING:
50
+ from adcp.client import ADCPClient
51
+
52
+
53
+ class SimpleAPI:
54
+ """Simplified API accessor for ergonomic usage.
55
+
56
+ Provides kwargs-based methods that return unwrapped response data
57
+ and raise exceptions on errors.
58
+
59
+ This is intended for:
60
+ - Quick prototyping and testing
61
+ - Documentation and examples
62
+ - Simple scripts and notebooks
63
+
64
+ For production code with complex error handling, use the standard
65
+ client API which returns TaskResult wrappers.
66
+ """
67
+
68
+ def __init__(self, client: ADCPClient):
69
+ """Initialize simple API accessor.
70
+
71
+ Args:
72
+ client: The ADCPClient instance to wrap
73
+ """
74
+ self._client = client
75
+
76
+ async def get_products(
77
+ self,
78
+ **kwargs: Any,
79
+ ) -> GetProductsResponse:
80
+ """Get advertising products (simplified).
81
+
82
+ This is a convenience wrapper around client.get_products() that:
83
+ - Accepts kwargs instead of GetProductsRequest
84
+ - Returns unwrapped GetProductsResponse
85
+ - Raises ADCPSimpleAPIError on failures
86
+
87
+ For full control over error handling, use client.get_products() instead.
88
+
89
+ Args:
90
+ **kwargs: Arguments for GetProductsRequest (brief, brand_manifest, etc.)
91
+
92
+ Returns:
93
+ GetProductsResponse directly (no TaskResult wrapper)
94
+
95
+ Raises:
96
+ ADCPSimpleAPIError: If request fails. Use standard API for detailed error handling.
97
+
98
+ Example:
99
+ products = await client.simple.get_products(
100
+ brief='Coffee subscription service'
101
+ )
102
+ print(f"Found {len(products.products)} products")
103
+ """
104
+ request = GetProductsRequest(**kwargs)
105
+ result = await self._client.get_products(request)
106
+ if not result.success or not result.data:
107
+ raise ADCPSimpleAPIError(
108
+ operation="get_products",
109
+ error_message=result.error,
110
+ agent_id=self._client.agent_config.id,
111
+ )
112
+ return result.data
113
+
114
+ async def list_creative_formats(
115
+ self,
116
+ **kwargs: Any,
117
+ ) -> ListCreativeFormatsResponse:
118
+ """List supported creative formats.
119
+
120
+ Args:
121
+ **kwargs: Arguments passed to ListCreativeFormatsRequest
122
+
123
+ Returns:
124
+ ListCreativeFormatsResponse with formats list
125
+
126
+ Raises:
127
+ Exception: If the request fails
128
+
129
+ Example:
130
+ formats = await client.simple.list_creative_formats()
131
+ print(f"Found {len(formats.formats)} formats")
132
+ """
133
+ request = ListCreativeFormatsRequest(**kwargs)
134
+ result = await self._client.list_creative_formats(request)
135
+ if not result.success or not result.data:
136
+ raise ADCPSimpleAPIError(
137
+ operation="list_creative_formats",
138
+ error_message=result.error,
139
+ agent_id=self._client.agent_config.id,
140
+ )
141
+ return result.data
142
+
143
+ async def preview_creative(
144
+ self,
145
+ **kwargs: Any,
146
+ ) -> PreviewCreativeResponse:
147
+ """Preview creative manifest.
148
+
149
+ Args:
150
+ **kwargs: Arguments passed to PreviewCreativeRequest
151
+
152
+ Returns:
153
+ PreviewCreativeResponse with preview data
154
+
155
+ Raises:
156
+ Exception: If the request fails
157
+
158
+ Example:
159
+ preview = await client.simple.preview_creative(
160
+ manifest={'format_id': 'banner_300x250', 'assets': {...}}
161
+ )
162
+ print(f"Preview: {preview.previews[0]}")
163
+ """
164
+ request = PreviewCreativeRequest(**kwargs)
165
+ result = await self._client.preview_creative(request)
166
+ if not result.success or not result.data:
167
+ raise ADCPSimpleAPIError(
168
+ operation="preview_creative",
169
+ error_message=result.error,
170
+ agent_id=self._client.agent_config.id,
171
+ )
172
+ return result.data
173
+
174
+ async def sync_creatives(
175
+ self,
176
+ **kwargs: Any,
177
+ ) -> SyncCreativesResponse:
178
+ """Sync creatives.
179
+
180
+ Args:
181
+ **kwargs: Arguments passed to SyncCreativesRequest
182
+
183
+ Returns:
184
+ SyncCreativesResponse
185
+
186
+ Raises:
187
+ Exception: If the request fails
188
+ """
189
+ request = SyncCreativesRequest(**kwargs)
190
+ result = await self._client.sync_creatives(request)
191
+ if not result.success or not result.data:
192
+ raise ADCPSimpleAPIError(
193
+ operation="sync_creatives",
194
+ error_message=result.error,
195
+ agent_id=self._client.agent_config.id,
196
+ )
197
+ return result.data
198
+
199
+ async def list_creatives(
200
+ self,
201
+ **kwargs: Any,
202
+ ) -> ListCreativesResponse:
203
+ """List creatives.
204
+
205
+ Args:
206
+ **kwargs: Arguments passed to ListCreativesRequest
207
+
208
+ Returns:
209
+ ListCreativesResponse
210
+
211
+ Raises:
212
+ Exception: If the request fails
213
+ """
214
+ request = ListCreativesRequest(**kwargs)
215
+ result = await self._client.list_creatives(request)
216
+ if not result.success or not result.data:
217
+ raise ADCPSimpleAPIError(
218
+ operation="list_creatives",
219
+ error_message=result.error,
220
+ agent_id=self._client.agent_config.id,
221
+ )
222
+ return result.data
223
+
224
+ async def get_media_buy_delivery(
225
+ self,
226
+ **kwargs: Any,
227
+ ) -> GetMediaBuyDeliveryResponse:
228
+ """Get media buy delivery.
229
+
230
+ Args:
231
+ **kwargs: Arguments passed to GetMediaBuyDeliveryRequest
232
+
233
+ Returns:
234
+ GetMediaBuyDeliveryResponse
235
+
236
+ Raises:
237
+ Exception: If the request fails
238
+ """
239
+ request = GetMediaBuyDeliveryRequest(**kwargs)
240
+ result = await self._client.get_media_buy_delivery(request)
241
+ if not result.success or not result.data:
242
+ raise ADCPSimpleAPIError(
243
+ operation="get_media_buy_delivery",
244
+ error_message=result.error,
245
+ agent_id=self._client.agent_config.id,
246
+ )
247
+ return result.data
248
+
249
+ async def list_authorized_properties(
250
+ self,
251
+ **kwargs: Any,
252
+ ) -> ListAuthorizedPropertiesResponse:
253
+ """List authorized properties.
254
+
255
+ Args:
256
+ **kwargs: Arguments passed to ListAuthorizedPropertiesRequest
257
+
258
+ Returns:
259
+ ListAuthorizedPropertiesResponse
260
+
261
+ Raises:
262
+ Exception: If the request fails
263
+ """
264
+ request = ListAuthorizedPropertiesRequest(**kwargs)
265
+ result = await self._client.list_authorized_properties(request)
266
+ if not result.success or not result.data:
267
+ raise ADCPSimpleAPIError(
268
+ operation="list_authorized_properties",
269
+ error_message=result.error,
270
+ agent_id=self._client.agent_config.id,
271
+ )
272
+ return result.data
273
+
274
+ async def get_signals(
275
+ self,
276
+ **kwargs: Any,
277
+ ) -> GetSignalsResponse:
278
+ """Get signals.
279
+
280
+ Args:
281
+ **kwargs: Arguments passed to GetSignalsRequest
282
+
283
+ Returns:
284
+ GetSignalsResponse
285
+
286
+ Raises:
287
+ Exception: If the request fails
288
+ """
289
+ request = GetSignalsRequest(**kwargs)
290
+ result = await self._client.get_signals(request)
291
+ if not result.success or not result.data:
292
+ raise ADCPSimpleAPIError(
293
+ operation="get_signals",
294
+ error_message=result.error,
295
+ agent_id=self._client.agent_config.id,
296
+ )
297
+ return result.data
298
+
299
+ async def activate_signal(
300
+ self,
301
+ **kwargs: Any,
302
+ ) -> ActivateSignalResponse:
303
+ """Activate signal.
304
+
305
+ Args:
306
+ **kwargs: Arguments passed to ActivateSignalRequest
307
+
308
+ Returns:
309
+ ActivateSignalResponse
310
+
311
+ Raises:
312
+ Exception: If the request fails
313
+ """
314
+ request = ActivateSignalRequest(**kwargs)
315
+ result = await self._client.activate_signal(request)
316
+ if not result.success or not result.data:
317
+ raise ADCPSimpleAPIError(
318
+ operation="activate_signal",
319
+ error_message=result.error,
320
+ agent_id=self._client.agent_config.id,
321
+ )
322
+ return result.data
323
+
324
+ async def provide_performance_feedback(
325
+ self,
326
+ **kwargs: Any,
327
+ ) -> ProvidePerformanceFeedbackResponse:
328
+ """Provide performance feedback.
329
+
330
+ Args:
331
+ **kwargs: Arguments passed to ProvidePerformanceFeedbackRequest
332
+
333
+ Returns:
334
+ ProvidePerformanceFeedbackResponse
335
+
336
+ Raises:
337
+ Exception: If the request fails
338
+ """
339
+ request = ProvidePerformanceFeedbackRequest(**kwargs)
340
+ result = await self._client.provide_performance_feedback(request)
341
+ if not result.success or not result.data:
342
+ raise ADCPSimpleAPIError(
343
+ operation="provide_performance_feedback",
344
+ error_message=result.error,
345
+ agent_id=self._client.agent_config.id,
346
+ )
347
+ return result.data
@@ -1,6 +1,21 @@
1
1
  """Test helpers for AdCP client library.
2
2
 
3
3
  Provides pre-configured test agents for examples and quick testing.
4
+
5
+ All test agents include a `.simple` accessor for ergonomic usage:
6
+
7
+ - **Standard API** (client methods): Full TaskResult with error handling
8
+ - **Simple API** (client.simple methods): Direct returns, raises on error
9
+
10
+ Example:
11
+ # Standard API - explicit control
12
+ result = await test_agent.get_products(GetProductsRequest(brief='Coffee'))
13
+ if result.success:
14
+ print(result.data.products)
15
+
16
+ # Simple API - ergonomic
17
+ products = await test_agent.simple.get_products(brief='Coffee')
18
+ print(products.products)
4
19
  """
5
20
 
6
21
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 1.3.1
3
+ Version: 1.4.1
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
@@ -63,36 +63,64 @@ pip install adcp
63
63
 
64
64
  ## Quick Start: Test Helpers
65
65
 
66
- The fastest way to get started is using the pre-configured test agents:
66
+ The fastest way to get started is using pre-configured test agents with the **`.simple` API**:
67
67
 
68
68
  ```python
69
69
  from adcp.testing import test_agent
70
- from adcp.types.generated import GetProductsRequest
71
70
 
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
- )
71
+ # Zero configuration - just import and call with kwargs!
72
+ products = await test_agent.simple.get_products(
73
+ brief='Coffee subscription service for busy professionals'
78
74
  )
79
75
 
80
- if result.success:
81
- print(f"Found {len(result.data.products)} products")
76
+ print(f"Found {len(products.products)} products")
77
+ ```
78
+
79
+ ### Simple vs. Standard API
80
+
81
+ **Every ADCPClient** includes both API styles via the `.simple` accessor:
82
+
83
+ **Simple API** (`client.simple.*`) - Recommended for examples/prototyping:
84
+ ```python
85
+ from adcp.testing import test_agent
86
+
87
+ # Kwargs and direct return - raises on error
88
+ products = await test_agent.simple.get_products(brief='Coffee brands')
89
+ print(products.products[0].name)
90
+ ```
91
+
92
+ **Standard API** (`client.*`) - Recommended for production:
93
+ ```python
94
+ from adcp.testing import test_agent
95
+ from adcp.types.generated import GetProductsRequest
96
+
97
+ # Explicit request objects and TaskResult wrapper
98
+ request = GetProductsRequest(brief='Coffee brands')
99
+ result = await test_agent.get_products(request)
100
+
101
+ if result.success and result.data:
102
+ print(result.data.products[0].name)
103
+ else:
104
+ print(f"Error: {result.error}")
82
105
  ```
83
106
 
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
107
+ **When to use which:**
108
+ - **Simple API** (`.simple`): Quick testing, documentation, examples, notebooks
109
+ - **Standard API**: Production code, complex error handling, webhook workflows
110
+
111
+ ### Available Test Helpers
112
+
113
+ Pre-configured agents (all include `.simple` accessor):
114
+ - **`test_agent`**: MCP test agent with authentication
115
+ - **`test_agent_a2a`**: A2A test agent with authentication
116
+ - **`test_agent_no_auth`**: MCP test agent without authentication
117
+ - **`test_agent_a2a_no_auth`**: A2A test agent without authentication
89
118
  - **`creative_agent`**: Reference creative agent for preview functionality
90
119
  - **`test_agent_client`**: Multi-agent client with both protocols
91
- - **`create_test_agent()`**: Factory for custom test configurations
92
120
 
93
121
  > **Note**: Test agents are rate-limited and for testing/examples only. DO NOT use in production.
94
122
 
95
- See [examples/test_helpers_demo.py](examples/test_helpers_demo.py) for more examples.
123
+ See [examples/simple_api_demo.py](examples/simple_api_demo.py) for a complete comparison.
96
124
 
97
125
  ## Quick Start: Distributed Operations
98
126
 
@@ -6,6 +6,7 @@ src/adcp/__main__.py
6
6
  src/adcp/client.py
7
7
  src/adcp/config.py
8
8
  src/adcp/exceptions.py
9
+ src/adcp/simple.py
9
10
  src/adcp.egg-info/PKG-INFO
10
11
  src/adcp.egg-info/SOURCES.txt
11
12
  src/adcp.egg-info/dependency_links.txt
@@ -34,4 +35,5 @@ tests/test_format_id_validation.py
34
35
  tests/test_helpers.py
35
36
  tests/test_preview_html.py
36
37
  tests/test_protocols.py
37
- tests/test_response_parser.py
38
+ tests/test_response_parser.py
39
+ tests/test_simple_api.py
@@ -164,6 +164,7 @@ class TestMCPAdapter:
164
164
  # Mock MCP result with structuredContent (required for AdCP)
165
165
  mock_result.content = [{"type": "text", "text": "Success"}]
166
166
  mock_result.structuredContent = {"products": [{"id": "prod1"}]}
167
+ mock_result.isError = False
167
168
  mock_session.call_tool.return_value = mock_result
168
169
 
169
170
  with patch.object(adapter, "_get_session", return_value=mock_session):
@@ -193,6 +194,7 @@ class TestMCPAdapter:
193
194
  # Mock MCP result with structuredContent (preferred over content)
194
195
  mock_result.content = [{"type": "text", "text": "Found 42 creative formats"}]
195
196
  mock_result.structuredContent = {"formats": [{"id": "format1"}, {"id": "format2"}]}
197
+ mock_result.isError = False
196
198
  mock_session.call_tool.return_value = mock_result
197
199
 
198
200
  with patch.object(adapter, "_get_session", return_value=mock_session):
@@ -207,24 +209,46 @@ class TestMCPAdapter:
207
209
 
208
210
  @pytest.mark.asyncio
209
211
  async def test_call_tool_missing_structured_content(self, mcp_config):
210
- """Test tool call fails when structuredContent is missing."""
212
+ """Test tool call fails when structuredContent is missing on successful response."""
211
213
  adapter = MCPAdapter(mcp_config)
212
214
 
213
215
  mock_session = AsyncMock()
214
216
  mock_result = MagicMock()
215
- # Mock MCP result WITHOUT structuredContent (invalid for AdCP)
217
+ # Mock MCP result WITHOUT structuredContent and isError=False (invalid)
216
218
  mock_result.content = [{"type": "text", "text": "Success"}]
217
219
  mock_result.structuredContent = None
220
+ mock_result.isError = False
218
221
  mock_session.call_tool.return_value = mock_result
219
222
 
220
223
  with patch.object(adapter, "_get_session", return_value=mock_session):
221
224
  result = await adapter._call_mcp_tool("get_products", {"brief": "test"})
222
225
 
223
- # Verify error handling for missing structuredContent
226
+ # Verify error handling for missing structuredContent on success
224
227
  assert result.success is False
225
228
  assert result.status == TaskStatus.FAILED
226
229
  assert "did not return structuredContent" in result.error
227
230
 
231
+ @pytest.mark.asyncio
232
+ async def test_call_tool_error_without_structured_content(self, mcp_config):
233
+ """Test tool call handles error responses without structuredContent gracefully."""
234
+ adapter = MCPAdapter(mcp_config)
235
+
236
+ mock_session = AsyncMock()
237
+ mock_result = MagicMock()
238
+ # Mock MCP error response WITHOUT structuredContent (valid for errors)
239
+ mock_result.content = [{"type": "text", "text": "brand_manifest must provide brand information"}]
240
+ mock_result.structuredContent = None
241
+ mock_result.isError = True
242
+ mock_session.call_tool.return_value = mock_result
243
+
244
+ with patch.object(adapter, "_get_session", return_value=mock_session):
245
+ result = await adapter._call_mcp_tool("get_products", {"brief": "test"})
246
+
247
+ # Verify error is handled gracefully
248
+ assert result.success is False
249
+ assert result.status == TaskStatus.FAILED
250
+ assert result.error == "brand_manifest must provide brand information"
251
+
228
252
  @pytest.mark.asyncio
229
253
  async def test_call_tool_error(self, mcp_config):
230
254
  """Test tool call error via MCP."""
@@ -0,0 +1,183 @@
1
+ """Tests for the simplified API accessor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, patch
6
+
7
+ import pytest
8
+
9
+ from adcp.testing import test_agent
10
+ from adcp.types.core import TaskResult, TaskStatus
11
+ from adcp.types.generated import (
12
+ GetProductsResponse,
13
+ ListCreativeFormatsResponse,
14
+ PreviewCreativeResponse,
15
+ Product,
16
+ )
17
+
18
+
19
+ @pytest.mark.asyncio
20
+ async def test_get_products_simple_api():
21
+ """Test client.simple.get_products with kwargs."""
22
+ # Create mock response (using model_construct to bypass validation for test data)
23
+ mock_product = Product.model_construct(
24
+ product_id="prod_1",
25
+ name="Test Product",
26
+ description="A test product",
27
+ )
28
+ mock_response = GetProductsResponse.model_construct(products=[mock_product])
29
+ mock_result = TaskResult[GetProductsResponse](
30
+ status=TaskStatus.COMPLETED, data=mock_response, success=True
31
+ )
32
+
33
+ # Mock the client's get_products method
34
+ with patch.object(test_agent, "get_products", new=AsyncMock(return_value=mock_result)):
35
+ # Call simplified API with kwargs
36
+ result = await test_agent.simple.get_products(brief="Coffee subscription service")
37
+
38
+ # Verify it returns unwrapped data
39
+ assert isinstance(result, GetProductsResponse)
40
+ assert len(result.products) == 1
41
+ assert result.products[0].product_id == "prod_1"
42
+
43
+ # Verify the underlying call was made correctly
44
+ test_agent.get_products.assert_called_once()
45
+ call_args = test_agent.get_products.call_args[0][0]
46
+ assert call_args.brief == "Coffee subscription service"
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_get_products_simple_api_failure():
51
+ """Test client.simple.get_products raises exception on failure."""
52
+ from adcp.exceptions import ADCPSimpleAPIError
53
+
54
+ # Create mock failure response
55
+ mock_result = TaskResult[GetProductsResponse](
56
+ status=TaskStatus.FAILED, data=None, success=False, error="Test error"
57
+ )
58
+
59
+ with patch.object(test_agent, "get_products", new=AsyncMock(return_value=mock_result)):
60
+ # Should raise ADCPSimpleAPIError on failure
61
+ with pytest.raises(ADCPSimpleAPIError, match="get_products failed"):
62
+ await test_agent.simple.get_products(brief="Test")
63
+
64
+
65
+ def test_simple_api_has_no_sync_methods():
66
+ """Test that simple API only provides async methods.
67
+
68
+ Users can wrap with asyncio.run() if they need sync behavior.
69
+ """
70
+ # Verify simple API doesn't have sync methods
71
+ assert not hasattr(test_agent.simple, "get_products_sync")
72
+ assert hasattr(test_agent.simple, "get_products")
73
+
74
+
75
+ @pytest.mark.asyncio
76
+ async def test_list_creative_formats_simple_api():
77
+ """Test client.simple.list_creative_formats with kwargs."""
78
+ from adcp.types.generated import Format
79
+
80
+ # Create mock response (using model_construct to bypass validation for test data)
81
+ mock_format = Format.model_construct(
82
+ format_id={"id": "banner_300x250"},
83
+ name="Banner 300x250",
84
+ description="Standard banner",
85
+ )
86
+ mock_response = ListCreativeFormatsResponse.model_construct(formats=[mock_format])
87
+ mock_result = TaskResult[ListCreativeFormatsResponse](
88
+ status=TaskStatus.COMPLETED, data=mock_response, success=True
89
+ )
90
+
91
+ with patch.object(test_agent, "list_creative_formats", new=AsyncMock(return_value=mock_result)):
92
+ # Call simplified API
93
+ result = await test_agent.simple.list_creative_formats()
94
+
95
+ # Verify it returns unwrapped data
96
+ assert isinstance(result, ListCreativeFormatsResponse)
97
+ assert len(result.formats) == 1
98
+ assert result.formats[0].format_id["id"] == "banner_300x250"
99
+
100
+
101
+ def test_simple_api_exists_on_client():
102
+ """Test that all clients have a .simple accessor."""
103
+ from adcp.testing import creative_agent, test_agent_a2a
104
+
105
+ # All clients should have .simple
106
+ assert hasattr(test_agent, "simple")
107
+ assert hasattr(test_agent_a2a, "simple")
108
+ assert hasattr(creative_agent, "simple")
109
+
110
+ # Should be SimpleAPI instance
111
+ from adcp.simple import SimpleAPI
112
+
113
+ assert isinstance(test_agent.simple, SimpleAPI)
114
+ assert isinstance(test_agent_a2a.simple, SimpleAPI)
115
+ assert isinstance(creative_agent.simple, SimpleAPI)
116
+
117
+
118
+ def test_simple_api_on_freshly_constructed_client():
119
+ """Test that .simple accessor works on freshly constructed ADCPClient."""
120
+ from adcp import ADCPClient, AgentConfig, Protocol
121
+ from adcp.simple import SimpleAPI
122
+
123
+ # Create a new client from scratch
124
+ client = ADCPClient(
125
+ AgentConfig(
126
+ id="test-agent",
127
+ agent_uri="https://test.example.com/mcp/",
128
+ protocol=Protocol.MCP,
129
+ auth_token="test-token",
130
+ )
131
+ )
132
+
133
+ # Should have .simple accessor
134
+ assert hasattr(client, "simple")
135
+ assert isinstance(client.simple, SimpleAPI)
136
+
137
+ # Should reference the same client
138
+ assert client.simple._client is client
139
+
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_preview_creative_simple_api():
143
+ """Test client.simple.preview_creative."""
144
+ from adcp.testing import creative_agent
145
+
146
+ mock_response = PreviewCreativeResponse(
147
+ previews=[{"url": "https://preview.example.com/123", "html": "<html>...</html>"}]
148
+ )
149
+ mock_result = TaskResult[PreviewCreativeResponse](
150
+ status=TaskStatus.COMPLETED, data=mock_response, success=True
151
+ )
152
+
153
+ with patch.object(creative_agent, "preview_creative", new=AsyncMock(return_value=mock_result)):
154
+ # Call simplified API
155
+ result = await creative_agent.simple.preview_creative(
156
+ manifest={"format_id": "banner_300x250", "assets": {}}
157
+ )
158
+
159
+ # Verify it returns unwrapped data
160
+ assert isinstance(result, PreviewCreativeResponse)
161
+ assert result.previews is not None
162
+ assert len(result.previews) == 1
163
+
164
+
165
+ def test_simple_api_methods():
166
+ """Test that SimpleAPI has all expected methods."""
167
+ # Check all methods exist
168
+ assert hasattr(test_agent.simple, "get_products")
169
+ assert hasattr(test_agent.simple, "list_creative_formats")
170
+ assert hasattr(test_agent.simple, "preview_creative")
171
+ assert hasattr(test_agent.simple, "sync_creatives")
172
+ assert hasattr(test_agent.simple, "list_creatives")
173
+ assert hasattr(test_agent.simple, "get_media_buy_delivery")
174
+ assert hasattr(test_agent.simple, "list_authorized_properties")
175
+ assert hasattr(test_agent.simple, "get_signals")
176
+ assert hasattr(test_agent.simple, "activate_signal")
177
+ assert hasattr(test_agent.simple, "provide_performance_feedback")
178
+
179
+ # Verify they're all async methods (not sync)
180
+ import inspect
181
+
182
+ assert inspect.iscoroutinefunction(test_agent.simple.get_products)
183
+ assert inspect.iscoroutinefunction(test_agent.simple.list_creative_formats)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes