adcp 1.1.0__py3-none-any.whl → 1.2.1__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 CHANGED
@@ -20,33 +20,120 @@ from adcp.exceptions import (
20
20
  )
21
21
  from adcp.types.core import AgentConfig, Protocol, TaskResult, TaskStatus, WebhookMetadata
22
22
  from adcp.types.generated import (
23
+ ActivateSignalError,
24
+ # Request/Response types
23
25
  ActivateSignalRequest,
24
26
  ActivateSignalResponse,
27
+ ActivateSignalSuccess,
28
+ ActivationKey,
29
+ AgentDeployment,
30
+ AgentDestination,
31
+ BothPreviewRender,
32
+ # Brand types
33
+ BrandManifest,
34
+ BrandManifestRef,
35
+ BuildCreativeRequest,
36
+ BuildCreativeResponse,
37
+ # Channel types
38
+ Channels,
39
+ CreateMediaBuyError,
25
40
  CreateMediaBuyRequest,
26
41
  CreateMediaBuyResponse,
42
+ CreateMediaBuySuccess,
43
+ # Creative types
44
+ CreativeAsset,
45
+ CreativeAssignment,
46
+ CreativeManifest,
47
+ CreativePolicy,
48
+ DaastAsset,
49
+ # Metrics types
50
+ DeliveryMetrics,
51
+ # Delivery types
52
+ DeliveryType,
53
+ Deployment,
54
+ # Deployment types
55
+ Destination,
56
+ Error,
57
+ Format,
58
+ FormatId,
59
+ FrequencyCap,
27
60
  GetMediaBuyDeliveryRequest,
28
61
  GetMediaBuyDeliveryResponse,
29
62
  GetProductsRequest,
30
63
  GetProductsResponse,
31
64
  GetSignalsRequest,
32
65
  GetSignalsResponse,
66
+ HtmlPreviewRender,
67
+ InlineDaastAsset,
68
+ InlineVastAsset,
69
+ Key_valueActivationKey,
33
70
  ListAuthorizedPropertiesRequest,
34
71
  ListAuthorizedPropertiesResponse,
35
72
  ListCreativeFormatsRequest,
36
73
  ListCreativeFormatsResponse,
37
74
  ListCreativesRequest,
38
75
  ListCreativesResponse,
76
+ Measurement,
77
+ # Core domain types
39
78
  MediaBuy,
79
+ # Status enums
80
+ MediaBuyStatus,
81
+ # Sub-asset types
82
+ MediaSubAsset,
83
+ Pacing,
84
+ Package,
85
+ PackageStatus,
86
+ PerformanceFeedback,
87
+ Placement,
88
+ PlatformDeployment,
89
+ PlatformDestination,
90
+ PreviewCreativeRequest,
91
+ PreviewCreativeResponse,
92
+ # Preview render types
93
+ PreviewRender,
94
+ PricingModel,
95
+ # Pricing types
96
+ PricingOption,
40
97
  Product,
98
+ PromotedProducts,
99
+ # Property and placement types
100
+ Property,
101
+ ProtocolEnvelope,
41
102
  ProvidePerformanceFeedbackRequest,
42
103
  ProvidePerformanceFeedbackResponse,
104
+ PushNotificationConfig,
105
+ ReportingCapabilities,
106
+ Response,
107
+ Segment_idActivationKey,
108
+ StandardFormatIds,
109
+ StartTiming,
110
+ SubAsset,
111
+ SyncCreativesError,
43
112
  SyncCreativesRequest,
44
113
  SyncCreativesResponse,
114
+ SyncCreativesSuccess,
115
+ # Targeting types
116
+ Targeting,
117
+ # Task types
118
+ TaskType,
119
+ TextSubAsset,
120
+ UpdateMediaBuyError,
45
121
  UpdateMediaBuyRequest,
46
122
  UpdateMediaBuyResponse,
123
+ UpdateMediaBuySuccess,
124
+ UrlDaastAsset,
125
+ UrlPreviewRender,
126
+ UrlVastAsset,
127
+ # Asset delivery types (VAST/DAAST)
128
+ VastAsset,
129
+ # Protocol types
130
+ WebhookPayload,
131
+ )
132
+ from adcp.types.generated import (
133
+ TaskStatus as GeneratedTaskStatus,
47
134
  )
48
135
 
49
- __version__ = "1.1.0"
136
+ __version__ = "1.2.1"
50
137
 
51
138
  __all__ = [
52
139
  # Client classes
@@ -67,30 +154,113 @@ __all__ = [
67
154
  "ADCPToolNotFoundError",
68
155
  "ADCPWebhookError",
69
156
  "ADCPWebhookSignatureError",
70
- # Generated request/response types
71
- "GetProductsRequest",
72
- "GetProductsResponse",
157
+ # Request/Response types
158
+ "ActivateSignalRequest",
159
+ "ActivateSignalResponse",
160
+ "ActivateSignalSuccess",
161
+ "ActivateSignalError",
162
+ "ActivationKey",
163
+ "Segment_idActivationKey",
164
+ "Key_valueActivationKey",
165
+ "BuildCreativeRequest",
166
+ "BuildCreativeResponse",
73
167
  "CreateMediaBuyRequest",
74
168
  "CreateMediaBuyResponse",
75
- "UpdateMediaBuyRequest",
76
- "UpdateMediaBuyResponse",
77
- "SyncCreativesRequest",
78
- "SyncCreativesResponse",
79
- "ListCreativesRequest",
80
- "ListCreativesResponse",
81
- "ListCreativeFormatsRequest",
82
- "ListCreativeFormatsResponse",
169
+ "CreateMediaBuySuccess",
170
+ "CreateMediaBuyError",
83
171
  "GetMediaBuyDeliveryRequest",
84
172
  "GetMediaBuyDeliveryResponse",
85
- "ListAuthorizedPropertiesRequest",
86
- "ListAuthorizedPropertiesResponse",
173
+ "GetProductsRequest",
174
+ "GetProductsResponse",
87
175
  "GetSignalsRequest",
88
176
  "GetSignalsResponse",
89
- "ActivateSignalRequest",
90
- "ActivateSignalResponse",
177
+ "ListAuthorizedPropertiesRequest",
178
+ "ListAuthorizedPropertiesResponse",
179
+ "ListCreativeFormatsRequest",
180
+ "ListCreativeFormatsResponse",
181
+ "ListCreativesRequest",
182
+ "ListCreativesResponse",
183
+ "PreviewCreativeRequest",
184
+ "PreviewCreativeResponse",
91
185
  "ProvidePerformanceFeedbackRequest",
92
186
  "ProvidePerformanceFeedbackResponse",
187
+ "SyncCreativesRequest",
188
+ "SyncCreativesResponse",
189
+ "SyncCreativesSuccess",
190
+ "SyncCreativesError",
191
+ "UpdateMediaBuyRequest",
192
+ "UpdateMediaBuyResponse",
193
+ "UpdateMediaBuySuccess",
194
+ "UpdateMediaBuyError",
93
195
  # Core domain types
94
- "Product",
95
196
  "MediaBuy",
197
+ "Product",
198
+ "Package",
199
+ "Error",
200
+ # Creative types
201
+ "CreativeAsset",
202
+ "CreativeManifest",
203
+ "CreativeAssignment",
204
+ "CreativePolicy",
205
+ "Format",
206
+ "FormatId",
207
+ # Property and placement types
208
+ "Property",
209
+ "Placement",
210
+ # Targeting types
211
+ "Targeting",
212
+ "FrequencyCap",
213
+ "Pacing",
214
+ # Brand types
215
+ "BrandManifest",
216
+ "BrandManifestRef",
217
+ # Metrics types
218
+ "DeliveryMetrics",
219
+ "Measurement",
220
+ "PerformanceFeedback",
221
+ # Status enums
222
+ "MediaBuyStatus",
223
+ "PackageStatus",
224
+ # Pricing types
225
+ "PricingOption",
226
+ "PricingModel",
227
+ # Delivery types
228
+ "DeliveryType",
229
+ "StartTiming",
230
+ # Channel types
231
+ "Channels",
232
+ "StandardFormatIds",
233
+ # Protocol types
234
+ "WebhookPayload",
235
+ "ProtocolEnvelope",
236
+ "Response",
237
+ "PromotedProducts",
238
+ "PushNotificationConfig",
239
+ "ReportingCapabilities",
240
+ # Deployment types
241
+ "Destination",
242
+ "Deployment",
243
+ "PlatformDestination",
244
+ "AgentDestination",
245
+ "PlatformDeployment",
246
+ "AgentDeployment",
247
+ # Sub-asset types
248
+ "MediaSubAsset",
249
+ "SubAsset",
250
+ "TextSubAsset",
251
+ # Asset delivery types (VAST/DAAST)
252
+ "VastAsset",
253
+ "UrlVastAsset",
254
+ "InlineVastAsset",
255
+ "DaastAsset",
256
+ "UrlDaastAsset",
257
+ "InlineDaastAsset",
258
+ # Preview render types
259
+ "PreviewRender",
260
+ "UrlPreviewRender",
261
+ "HtmlPreviewRender",
262
+ "BothPreviewRender",
263
+ # Task types
264
+ "TaskType",
265
+ "GeneratedTaskStatus",
96
266
  ]
adcp/client.py CHANGED
@@ -11,6 +11,8 @@ from collections.abc import Callable
11
11
  from datetime import datetime, timezone
12
12
  from typing import Any
13
13
 
14
+ from pydantic import BaseModel
15
+
14
16
  from adcp.exceptions import ADCPWebhookSignatureError
15
17
  from adcp.protocols.a2a import A2AAdapter
16
18
  from adcp.protocols.base import ProtocolAdapter
@@ -154,7 +156,9 @@ class ADCPClient:
154
156
  )
155
157
  )
156
158
 
157
- result = self.adapter._parse_response(raw_result, GetProductsResponse)
159
+ result: TaskResult[GetProductsResponse] = self.adapter._parse_response(
160
+ raw_result, GetProductsResponse
161
+ )
158
162
 
159
163
  if fetch_previews and result.success and result.data and creative_agent_client:
160
164
  from adcp.utils.preview_cache import add_preview_urls_to_products
@@ -215,7 +219,9 @@ class ADCPClient:
215
219
  )
216
220
  )
217
221
 
218
- result = self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
222
+ result: TaskResult[ListCreativeFormatsResponse] = self.adapter._parse_response(
223
+ raw_result, ListCreativeFormatsResponse
224
+ )
219
225
 
220
226
  if fetch_previews and result.success and result.data:
221
227
  from adcp.utils.preview_cache import add_preview_urls_to_formats
@@ -617,15 +623,16 @@ class ADCPClient:
617
623
  from adcp.utils.response_parser import parse_json_or_text
618
624
 
619
625
  # Map task types to their response types (using string literals, not enum)
620
- response_type_map: dict[str, type] = {
626
+ # Note: Some response types are Union types (e.g., ActivateSignalResponse = Success | Error)
627
+ response_type_map: dict[str, type[BaseModel] | Any] = {
621
628
  "get_products": GetProductsResponse,
622
629
  "list_creative_formats": ListCreativeFormatsResponse,
623
- "sync_creatives": SyncCreativesResponse,
630
+ "sync_creatives": SyncCreativesResponse, # Union type
624
631
  "list_creatives": ListCreativesResponse,
625
632
  "get_media_buy_delivery": GetMediaBuyDeliveryResponse,
626
633
  "list_authorized_properties": ListAuthorizedPropertiesResponse,
627
634
  "get_signals": GetSignalsResponse,
628
- "activate_signal": ActivateSignalResponse,
635
+ "activate_signal": ActivateSignalResponse, # Union type
629
636
  "provide_performance_feedback": ProvidePerformanceFeedbackResponse,
630
637
  }
631
638
 
adcp/protocols/base.py CHANGED
@@ -30,15 +30,18 @@ class ProtocolAdapter(ABC):
30
30
  # Helper methods for response parsing
31
31
  # ========================================================================
32
32
 
33
- def _parse_response(self, raw_result: TaskResult[Any], response_type: type[T]) -> TaskResult[T]:
33
+ def _parse_response(
34
+ self, raw_result: TaskResult[Any], response_type: type[T] | Any
35
+ ) -> TaskResult[T]:
34
36
  """
35
37
  Parse raw TaskResult into typed TaskResult.
36
38
 
37
39
  Handles both MCP content arrays and A2A dict responses.
40
+ Supports both single types and Union types (for oneOf discriminated unions).
38
41
 
39
42
  Args:
40
43
  raw_result: Raw TaskResult from adapter
41
- response_type: Expected Pydantic response type
44
+ response_type: Expected Pydantic response type (can be a Union type)
42
45
 
43
46
  Returns:
44
47
  Typed TaskResult
adcp/types/generated.py CHANGED
@@ -14,19 +14,9 @@ from __future__ import annotations
14
14
  import re
15
15
  from typing import Any, Literal
16
16
 
17
- from pydantic import BaseModel, Field, field_validator
17
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
18
18
 
19
19
 
20
- # ============================================================================
21
- # MISSING SCHEMA TYPES (referenced but not provided by upstream)
22
- # ============================================================================
23
-
24
- # These types are referenced in schemas but don't have schema files
25
- # Defining them as type aliases to maintain type safety
26
- PackageRequest = dict[str, Any]
27
- PushNotificationConfig = dict[str, Any]
28
- ReportingCapabilities = dict[str, Any]
29
-
30
20
 
31
21
  # ============================================================================
32
22
  # CORE DOMAIN TYPES
@@ -125,9 +115,22 @@ class BrandManifest(BaseModel):
125
115
  metadata: dict[str, Any] | None = Field(None, description="Additional brand metadata")
126
116
 
127
117
 
128
- # Type alias for Brand Manifest Reference
129
118
  # Brand manifest provided either as an inline object or a URL string pointing to a hosted manifest
130
- BrandManifestRef = Any
119
+
120
+ class BrandManifestRefVariant1(BaseModel):
121
+ """Inline brand manifest object"""
122
+
123
+ pass
124
+
125
+
126
+ class BrandManifestRefVariant2(BaseModel):
127
+ """URL to a hosted brand manifest JSON file. The manifest at this URL must conform to the brand-manifest.json schema."""
128
+
129
+ pass
130
+
131
+
132
+ # Union type for Brand Manifest Reference
133
+ BrandManifestRef = BrandManifestRefVariant1 | BrandManifestRefVariant2
131
134
 
132
135
 
133
136
  class Format(BaseModel):
@@ -255,18 +258,46 @@ class PerformanceFeedback(BaseModel):
255
258
  applied_at: str | None = Field(None, description="ISO 8601 timestamp when feedback was applied to optimization algorithms")
256
259
 
257
260
 
258
- # Type alias for Start Timing
259
261
  # Campaign start timing: 'asap' or ISO 8601 date-time
260
- StartTiming = Any
261
262
 
263
+ class StartTimingVariant1(BaseModel):
264
+ """Start campaign as soon as possible"""
265
+
266
+ pass
267
+
268
+
269
+ class StartTimingVariant2(BaseModel):
270
+ """Scheduled start date/time in ISO 8601 format"""
271
+
272
+ pass
273
+
274
+
275
+ # Union type for Start Timing
276
+ StartTiming = StartTimingVariant1 | StartTimingVariant2
277
+
278
+
279
+ # Sub-asset for multi-asset creative formats, including carousel images and native ad template variables
280
+
281
+ class MediaSubAsset(BaseModel):
282
+ model_config = ConfigDict(extra="forbid")
262
283
 
263
- class SubAsset(BaseModel):
264
- """Sub-asset for multi-asset creative formats, including carousel images and native ad template variables"""
284
+ asset_kind: Literal["media"] = Field(description="Discriminator indicating this is a media asset with content_uri")
285
+ asset_type: str = Field(description="Type of asset. Common types: thumbnail_image, product_image, featured_image, logo")
286
+ asset_id: str = Field(description="Unique identifier for the asset within the creative")
287
+ content_uri: str = Field(description="URL for media assets (images, videos, etc.)")
265
288
 
266
- asset_type: str | None = Field(None, description="Type of asset. Common types: headline, body_text, thumbnail_image, product_image, featured_image, logo, cta_text, price_text, sponsor_name, author_name, click_url")
267
- asset_id: str | None = Field(None, description="Unique identifier for the asset within the creative")
268
- content_uri: str | None = Field(None, description="URL for media assets (images, videos, etc.)")
269
- content: Any | None = Field(None, description="Text content for text-based assets like headlines, body text, CTA text, etc.")
289
+
290
+ class TextSubAsset(BaseModel):
291
+ model_config = ConfigDict(extra="forbid")
292
+
293
+ asset_kind: Literal["text"] = Field(description="Discriminator indicating this is a text asset with content")
294
+ asset_type: str = Field(description="Type of asset. Common types: headline, body_text, cta_text, price_text, sponsor_name, author_name, click_url")
295
+ asset_id: str = Field(description="Unique identifier for the asset within the creative")
296
+ content: Any = Field(description="Text content for text-based assets like headlines, body text, CTA text, etc.")
297
+
298
+
299
+ # Union type for Sub-Asset
300
+ SubAsset = MediaSubAsset | TextSubAsset
270
301
 
271
302
 
272
303
  class WebhookPayload(BaseModel):
@@ -281,7 +312,7 @@ class WebhookPayload(BaseModel):
281
312
  message: str | None = Field(None, description="Human-readable summary of the current task state. Provides context about what happened and what action may be needed.")
282
313
  context_id: str | None = Field(None, description="Session/conversation identifier. Use this to continue the conversation if input-required status needs clarification or additional parameters.")
283
314
  progress: dict[str, Any] | None = Field(None, description="Progress information for tasks still in 'working' state. Rarely seen in webhooks since 'working' tasks typically complete synchronously, but may appear if a task transitions from 'submitted' to 'working'.")
284
- result: Any | None = Field(None, description="Task-specific payload for this status update. For 'completed', contains the final result. For 'input-required', may contain approval or clarification context. Optional for non-terminal updates.")
315
+ result: Any | None = Field(None, description="Task-specific payload for this status update. Validated against the appropriate response schema based on task_type.")
285
316
  error: Any | None = Field(None, description="Error message for failed tasks. Only present when status is 'failed'.")
286
317
 
287
318
 
@@ -314,6 +345,97 @@ class PromotedProducts(BaseModel):
314
345
  manifest_query: str | None = Field(None, description="Natural language query to select products from the brand manifest (e.g., 'all Kraft Heinz pasta sauces', 'organic products under $20')")
315
346
 
316
347
 
348
+ # A destination platform where signals can be activated (DSP, sales agent, etc.)
349
+
350
+ class PlatformDestination(BaseModel):
351
+ model_config = ConfigDict(extra="forbid")
352
+
353
+ type: Literal["platform"] = Field(description="Discriminator indicating this is a platform-based destination")
354
+ platform: str = Field(description="Platform identifier for DSPs (e.g., 'the-trade-desk', 'amazon-dsp')")
355
+ account: str | None = Field(None, description="Optional account identifier on the platform")
356
+
357
+
358
+ class AgentDestination(BaseModel):
359
+ model_config = ConfigDict(extra="forbid")
360
+
361
+ type: Literal["agent"] = Field(description="Discriminator indicating this is an agent URL-based destination")
362
+ agent_url: str = Field(description="URL identifying the destination agent (for sales agents, etc.)")
363
+ account: str | None = Field(None, description="Optional account identifier on the agent")
364
+
365
+
366
+ # Union type for Destination
367
+ Destination = PlatformDestination | AgentDestination
368
+
369
+
370
+ # A signal deployment to a specific destination platform with activation status and key
371
+
372
+ class PlatformDeployment(BaseModel):
373
+ model_config = ConfigDict(extra="forbid")
374
+
375
+ type: Literal["platform"] = Field(description="Discriminator indicating this is a platform-based deployment")
376
+ platform: str = Field(description="Platform identifier for DSPs")
377
+ account: str | None = Field(None, description="Account identifier if applicable")
378
+ is_live: bool = Field(description="Whether signal is currently active on this destination")
379
+ activation_key: ActivationKey | None = Field(None, description="The key to use for targeting. Only present if is_live=true AND requester has access to this destination.")
380
+ estimated_activation_duration_minutes: float | None = Field(None, description="Estimated time to activate if not live, or to complete activation if in progress")
381
+ deployed_at: str | None = Field(None, description="Timestamp when activation completed (if is_live=true)")
382
+
383
+
384
+ class AgentDeployment(BaseModel):
385
+ model_config = ConfigDict(extra="forbid")
386
+
387
+ type: Literal["agent"] = Field(description="Discriminator indicating this is an agent URL-based deployment")
388
+ agent_url: str = Field(description="URL identifying the destination agent")
389
+ account: str | None = Field(None, description="Account identifier if applicable")
390
+ is_live: bool = Field(description="Whether signal is currently active on this destination")
391
+ activation_key: ActivationKey | None = Field(None, description="The key to use for targeting. Only present if is_live=true AND requester has access to this destination.")
392
+ estimated_activation_duration_minutes: float | None = Field(None, description="Estimated time to activate if not live, or to complete activation if in progress")
393
+ deployed_at: str | None = Field(None, description="Timestamp when activation completed (if is_live=true)")
394
+
395
+
396
+ # Union type for Deployment
397
+ Deployment = PlatformDeployment | AgentDeployment
398
+
399
+
400
+ # Universal identifier for using a signal on a destination platform. Can be either a segment ID or a key-value pair depending on the platform's targeting mechanism.
401
+
402
+ class Segment_idActivationKey(BaseModel):
403
+ model_config = ConfigDict(extra="forbid")
404
+
405
+ type: Literal["segment_id"] = Field(description="Segment ID based targeting")
406
+ segment_id: str = Field(description="The platform-specific segment identifier to use in campaign targeting")
407
+
408
+
409
+ class Key_valueActivationKey(BaseModel):
410
+ model_config = ConfigDict(extra="forbid")
411
+
412
+ type: Literal["key_value"] = Field(description="Key-value pair based targeting")
413
+ key: str = Field(description="The targeting parameter key")
414
+ value: str = Field(description="The targeting parameter value")
415
+
416
+
417
+ # Union type for Activation Key
418
+ ActivationKey = Segment_idActivationKey | Key_valueActivationKey
419
+
420
+
421
+ class PushNotificationConfig(BaseModel):
422
+ """Webhook configuration for asynchronous task notifications. Uses A2A-compatible PushNotificationConfig structure. Supports Bearer tokens (simple) or HMAC signatures (production-recommended)."""
423
+
424
+ url: str = Field(description="Webhook endpoint URL for task status notifications")
425
+ token: str | None = Field(None, description="Optional client-provided token for webhook validation. Echoed back in webhook payload to validate request authenticity.")
426
+ authentication: dict[str, Any] = Field(description="Authentication configuration for webhook delivery (A2A-compatible)")
427
+
428
+
429
+ class ReportingCapabilities(BaseModel):
430
+ """Reporting capabilities available for a product"""
431
+
432
+ available_reporting_frequencies: list[Literal["hourly", "daily", "monthly"]] = Field(description="Supported reporting frequency options")
433
+ expected_delay_minutes: int = Field(description="Expected delay in minutes before reporting data becomes available (e.g., 240 for 4-hour delay)")
434
+ timezone: str = Field(description="Timezone for reporting periods. Use 'UTC' or IANA timezone (e.g., 'America/New_York'). Critical for daily/monthly frequency alignment.")
435
+ supports_webhooks: bool = Field(description="Whether this product supports webhook-based reporting notifications")
436
+ available_metrics: list[Literal["impressions", "spend", "clicks", "ctr", "video_completions", "completion_rate", "conversions", "viewability", "engagement_rate"]] = Field(description="Metrics available in reporting. Impressions and spend are always implicitly included.")
437
+
438
+
317
439
  # Type alias for Advertising Channels
318
440
  # Standard advertising channels supported by AdCP
319
441
  Channels = Literal["display", "video", "audio", "native", "dooh", "ctv", "podcast", "retail", "social"]
@@ -354,9 +476,46 @@ TaskStatus = Literal["submitted", "working", "input-required", "completed", "can
354
476
  PricingModel = Literal["cpm", "vcpm", "cpc", "cpcv", "cpv", "cpp", "flat_rate"]
355
477
 
356
478
 
357
- # Type alias for Pricing Option
358
479
  # A pricing model option offered by a publisher for a product. Each pricing model has its own schema with model-specific requirements.
359
- PricingOption = Any
480
+
481
+ class PricingOptionVariant1(BaseModel):
482
+ pass
483
+
484
+
485
+ class PricingOptionVariant2(BaseModel):
486
+ pass
487
+
488
+
489
+ class PricingOptionVariant3(BaseModel):
490
+ pass
491
+
492
+
493
+ class PricingOptionVariant4(BaseModel):
494
+ pass
495
+
496
+
497
+ class PricingOptionVariant5(BaseModel):
498
+ pass
499
+
500
+
501
+ class PricingOptionVariant6(BaseModel):
502
+ pass
503
+
504
+
505
+ class PricingOptionVariant7(BaseModel):
506
+ pass
507
+
508
+
509
+ class PricingOptionVariant8(BaseModel):
510
+ pass
511
+
512
+
513
+ class PricingOptionVariant9(BaseModel):
514
+ pass
515
+
516
+
517
+ # Union type for Pricing Option
518
+ PricingOption = PricingOptionVariant1 | PricingOptionVariant2 | PricingOptionVariant3 | PricingOptionVariant4 | PricingOptionVariant5 | PricingOptionVariant6 | PricingOptionVariant7 | PricingOptionVariant8 | PricingOptionVariant9
360
519
 
361
520
 
362
521
  # Type alias for Standard Format IDs
@@ -364,17 +523,118 @@ PricingOption = Any
364
523
  StandardFormatIds = Literal["display_300x250", "display_728x90", "display_320x50", "display_160x600", "display_970x250", "display_336x280", "display_expandable_300x250", "display_expandable_728x90", "display_interstitial_320x480", "display_interstitial_desktop", "display_dynamic_300x250", "display_responsive", "native_in_feed", "native_content_recommendation", "native_product", "video_skippable_15s", "video_skippable_30s", "video_non_skippable_15s", "video_non_skippable_30s", "video_outstream_autoplay", "video_vertical_story", "video_rewarded_30s", "video_pause_ad", "video_ctv_non_skippable_30s", "audio_standard_15s", "audio_standard_30s", "audio_podcast_host_read", "audio_programmatic", "universal_carousel", "universal_canvas", "universal_takeover", "universal_gallery", "universal_reveal", "dooh_landscape_static", "dooh_portrait_video"]
365
524
 
366
525
 
526
+ # VAST (Video Ad Serving Template) tag for third-party video ad serving
527
+
528
+ class UrlVastAsset(BaseModel):
529
+ model_config = ConfigDict(extra="forbid")
530
+
531
+ delivery_type: Literal["url"] = Field(description="Discriminator indicating VAST is delivered via URL endpoint")
532
+ url: str = Field(description="URL endpoint that returns VAST XML")
533
+ vast_version: Literal["2.0", "3.0", "4.0", "4.1", "4.2"] | None = Field(None, description="VAST specification version")
534
+ vpaid_enabled: bool | None = Field(None, description="Whether VPAID (Video Player-Ad Interface Definition) is supported")
535
+ duration_ms: int | None = Field(None, description="Expected video duration in milliseconds (if known)")
536
+ tracking_events: list[Literal["start", "firstQuartile", "midpoint", "thirdQuartile", "complete", "impression", "click", "pause", "resume", "skip", "mute", "unmute", "fullscreen", "exitFullscreen", "playerExpand", "playerCollapse"]] | None = Field(None, description="Tracking events supported by this VAST tag")
537
+
538
+
539
+ class InlineVastAsset(BaseModel):
540
+ model_config = ConfigDict(extra="forbid")
541
+
542
+ delivery_type: Literal["inline"] = Field(description="Discriminator indicating VAST is delivered as inline XML content")
543
+ content: str = Field(description="Inline VAST XML content")
544
+ vast_version: Literal["2.0", "3.0", "4.0", "4.1", "4.2"] | None = Field(None, description="VAST specification version")
545
+ vpaid_enabled: bool | None = Field(None, description="Whether VPAID (Video Player-Ad Interface Definition) is supported")
546
+ duration_ms: int | None = Field(None, description="Expected video duration in milliseconds (if known)")
547
+ tracking_events: list[Literal["start", "firstQuartile", "midpoint", "thirdQuartile", "complete", "impression", "click", "pause", "resume", "skip", "mute", "unmute", "fullscreen", "exitFullscreen", "playerExpand", "playerCollapse"]] | None = Field(None, description="Tracking events supported by this VAST tag")
548
+
549
+
550
+ # Union type for VAST Asset
551
+ VastAsset = UrlVastAsset | InlineVastAsset
552
+
553
+
554
+ # DAAST (Digital Audio Ad Serving Template) tag for third-party audio ad serving
555
+
556
+ class UrlDaastAsset(BaseModel):
557
+ model_config = ConfigDict(extra="forbid")
558
+
559
+ delivery_type: Literal["url"] = Field(description="Discriminator indicating DAAST is delivered via URL endpoint")
560
+ url: str = Field(description="URL endpoint that returns DAAST XML")
561
+ daast_version: Literal["1.0", "1.1"] | None = Field(None, description="DAAST specification version")
562
+ duration_ms: int | None = Field(None, description="Expected audio duration in milliseconds (if known)")
563
+ tracking_events: list[Literal["start", "firstQuartile", "midpoint", "thirdQuartile", "complete", "impression", "pause", "resume", "skip", "mute", "unmute"]] | None = Field(None, description="Tracking events supported by this DAAST tag")
564
+ companion_ads: bool | None = Field(None, description="Whether companion display ads are included")
565
+
566
+
567
+ class InlineDaastAsset(BaseModel):
568
+ model_config = ConfigDict(extra="forbid")
569
+
570
+ delivery_type: Literal["inline"] = Field(description="Discriminator indicating DAAST is delivered as inline XML content")
571
+ content: str = Field(description="Inline DAAST XML content")
572
+ daast_version: Literal["1.0", "1.1"] | None = Field(None, description="DAAST specification version")
573
+ duration_ms: int | None = Field(None, description="Expected audio duration in milliseconds (if known)")
574
+ tracking_events: list[Literal["start", "firstQuartile", "midpoint", "thirdQuartile", "complete", "impression", "pause", "resume", "skip", "mute", "unmute"]] | None = Field(None, description="Tracking events supported by this DAAST tag")
575
+ companion_ads: bool | None = Field(None, description="Whether companion display ads are included")
576
+
577
+
578
+ # Union type for DAAST Asset
579
+ DaastAsset = UrlDaastAsset | InlineDaastAsset
580
+
581
+
582
+ # A single rendered piece of a creative preview with discriminated output format
583
+
584
+ class UrlPreviewRender(BaseModel):
585
+ """URL-only preview format"""
586
+
587
+ model_config = ConfigDict(extra="forbid")
588
+
589
+ render_id: str = Field(description="Unique identifier for this rendered piece within the variant")
590
+ output_format: Literal["url"] = Field(description="Discriminator indicating preview_url is provided")
591
+ preview_url: str = Field(description="URL to an HTML page that renders this piece. Can be embedded in an iframe.")
592
+ role: str = Field(description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles.")
593
+ dimensions: dict[str, Any] | None = Field(None, description="Dimensions for this rendered piece")
594
+ embedding: dict[str, Any] | None = Field(None, description="Optional security and embedding metadata for safe iframe integration")
595
+
596
+
597
+ class HtmlPreviewRender(BaseModel):
598
+ """HTML-only preview format"""
599
+
600
+ model_config = ConfigDict(extra="forbid")
601
+
602
+ render_id: str = Field(description="Unique identifier for this rendered piece within the variant")
603
+ output_format: Literal["html"] = Field(description="Discriminator indicating preview_html is provided")
604
+ preview_html: str = Field(description="Raw HTML for this rendered piece. Can be embedded directly in the page without iframe. Security warning: Only use with trusted creative agents as this bypasses iframe sandboxing.")
605
+ role: str = Field(description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles.")
606
+ dimensions: dict[str, Any] | None = Field(None, description="Dimensions for this rendered piece")
607
+ embedding: dict[str, Any] | None = Field(None, description="Optional security and embedding metadata")
608
+
609
+
610
+ class BothPreviewRender(BaseModel):
611
+ """Both URL and HTML preview format"""
612
+
613
+ model_config = ConfigDict(extra="forbid")
614
+
615
+ render_id: str = Field(description="Unique identifier for this rendered piece within the variant")
616
+ output_format: Literal["both"] = Field(description="Discriminator indicating both preview_url and preview_html are provided")
617
+ preview_url: str = Field(description="URL to an HTML page that renders this piece. Can be embedded in an iframe.")
618
+ preview_html: str = Field(description="Raw HTML for this rendered piece. Can be embedded directly in the page without iframe. Security warning: Only use with trusted creative agents as this bypasses iframe sandboxing.")
619
+ role: str = Field(description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles.")
620
+ dimensions: dict[str, Any] | None = Field(None, description="Dimensions for this rendered piece")
621
+ embedding: dict[str, Any] | None = Field(None, description="Optional security and embedding metadata for safe iframe integration")
622
+
623
+
624
+ # Union type for Preview Render
625
+ PreviewRender = UrlPreviewRender | HtmlPreviewRender | BothPreviewRender
626
+
627
+
367
628
 
368
629
  # ============================================================================
369
630
  # TASK REQUEST/RESPONSE TYPES
370
631
  # ============================================================================
371
632
 
372
633
  class ActivateSignalRequest(BaseModel):
373
- """Request parameters for activating a signal on a specific platform/account"""
634
+ """Request parameters for activating a signal on a specific destination"""
374
635
 
375
636
  signal_agent_segment_id: str = Field(description="The universal identifier for the signal to activate")
376
- platform: str = Field(description="The target platform for activation")
377
- account: str | None = Field(None, description="Account identifier (required for account-specific activation)")
637
+ destinations: list[Destination] = Field(description="Target destination(s) for activation. If the authenticated caller matches one of these destinations, activation keys will be included in the response.")
378
638
 
379
639
 
380
640
  class BuildCreativeRequest(BaseModel):
@@ -419,7 +679,7 @@ class GetSignalsRequest(BaseModel):
419
679
  """Request parameters for discovering signals based on description"""
420
680
 
421
681
  signal_spec: str = Field(description="Natural language description of the desired signals")
422
- deliver_to: dict[str, Any] = Field(description="Where the signals need to be delivered")
682
+ deliver_to: dict[str, Any] = Field(description="Destination platforms where signals need to be activated")
423
683
  filters: dict[str, Any] | None = Field(None, description="Filters to refine results")
424
684
  max_results: int | None = Field(None, description="Maximum number of results to return")
425
685
 
@@ -456,6 +716,21 @@ class ListCreativesRequest(BaseModel):
456
716
  fields: list[Literal["creative_id", "name", "format", "status", "created_date", "updated_date", "tags", "assignments", "performance", "sub_assets"]] | None = Field(None, description="Specific fields to include in response (omit for all fields)")
457
717
 
458
718
 
719
+ class PackageRequest(BaseModel):
720
+ """Package configuration for media buy creation"""
721
+
722
+ buyer_ref: str = Field(description="Buyer's reference identifier for this package")
723
+ product_id: str = Field(description="Product ID for this package")
724
+ format_ids: list[FormatId] | None = Field(None, description="Array of format IDs that will be used for this package - must be supported by the product. If omitted, defaults to all formats supported by the product.")
725
+ budget: float = Field(description="Budget allocation for this package in the media buy's currency")
726
+ pacing: Pacing | None = None
727
+ pricing_option_id: str = Field(description="ID of the selected pricing option from the product's pricing_options array")
728
+ bid_price: float | None = Field(None, description="Bid price for auction-based CPM pricing (required if using cpm-auction-option)")
729
+ targeting_overlay: Targeting | None = None
730
+ creative_ids: list[str] | None = Field(None, description="Creative IDs to assign to this package at creation time (references existing library creatives)")
731
+ creatives: list[CreativeAsset] | None = Field(None, description="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.")
732
+
733
+
459
734
  class ProvidePerformanceFeedbackRequest(BaseModel):
460
735
  """Request payload for provide_performance_feedback task"""
461
736
 
@@ -480,6 +755,22 @@ class SyncCreativesRequest(BaseModel):
480
755
  push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async sync notifications. Publisher will send webhook when sync completes if operation takes longer than immediate response time (typically for large bulk operations or manual approval/HITL).")
481
756
 
482
757
 
758
+ class TasksGetRequest(BaseModel):
759
+ """Request parameters for retrieving a specific task by ID with optional conversation history across all AdCP domains"""
760
+
761
+ task_id: str = Field(description="Unique identifier of the task to retrieve")
762
+ include_history: bool | None = Field(None, description="Include full conversation history for this task (may increase response size)")
763
+
764
+
765
+ class TasksListRequest(BaseModel):
766
+ """Request parameters for listing and filtering async tasks across all AdCP domains with state reconciliation capabilities"""
767
+
768
+ filters: dict[str, Any] | None = Field(None, description="Filter criteria for querying tasks")
769
+ sort: dict[str, Any] | None = Field(None, description="Sorting parameters")
770
+ pagination: dict[str, Any] | None = Field(None, description="Pagination parameters")
771
+ include_history: bool | None = Field(None, description="Include full conversation history for each task (may significantly increase response size)")
772
+
773
+
483
774
  class UpdateMediaBuyRequest(BaseModel):
484
775
  """Request parameters for updating campaign and package settings"""
485
776
 
@@ -492,30 +783,26 @@ class UpdateMediaBuyRequest(BaseModel):
492
783
  push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.")
493
784
 
494
785
 
495
- class ActivateSignalResponse(BaseModel):
496
- """Response payload for activate_signal task"""
786
+ # Response containing the transformed or generated creative manifest, ready for use with preview_creative or sync_creatives. Returns either the complete creative manifest OR error information, never both.
497
787
 
498
- decisioning_platform_segment_id: str | None = Field(None, description="The platform-specific ID to use once activated")
499
- estimated_activation_duration_minutes: float | None = Field(None, description="Estimated time to complete (optional)")
500
- deployed_at: str | None = Field(None, description="Timestamp when activation completed (optional)")
501
- errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., activation failures, platform issues)")
788
+ class BuildCreativeResponseVariant1(BaseModel):
789
+ """Success response - creative manifest generated successfully"""
502
790
 
503
-
504
- class BuildCreativeResponse(BaseModel):
505
- """Response containing the transformed or generated creative manifest, ready for use with preview_creative or sync_creatives"""
791
+ model_config = ConfigDict(extra="forbid")
506
792
 
507
793
  creative_manifest: CreativeManifest = Field(description="The generated or transformed creative manifest")
508
- errors: list[Error] | None = Field(None, description="Task-specific errors and warnings")
509
794
 
510
795
 
511
- class CreateMediaBuyResponse(BaseModel):
512
- """Response payload for create_media_buy task"""
796
+ class BuildCreativeResponseVariant2(BaseModel):
797
+ """Error response - creative generation failed"""
513
798
 
514
- media_buy_id: str | None = Field(None, description="Publisher's unique identifier for the created media buy")
515
- buyer_ref: str = Field(description="Buyer's reference identifier for this media buy")
516
- creative_deadline: str | None = Field(None, description="ISO 8601 timestamp for creative upload deadline")
517
- packages: list[dict[str, Any]] | None = Field(None, description="Array of created packages")
518
- errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., partial package creation failures)")
799
+ model_config = ConfigDict(extra="forbid")
800
+
801
+ errors: list[Error] = Field(description="Array of errors explaining why creative generation failed")
802
+
803
+
804
+ # Union type for Build Creative Response
805
+ BuildCreativeResponse = BuildCreativeResponseVariant1 | BuildCreativeResponseVariant2
519
806
 
520
807
 
521
808
  class GetMediaBuyDeliveryResponse(BaseModel):
@@ -577,28 +864,50 @@ class ListCreativesResponse(BaseModel):
577
864
  status_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by status")
578
865
 
579
866
 
580
- class ProvidePerformanceFeedbackResponse(BaseModel):
581
- """Response payload for provide_performance_feedback task"""
867
+ # Response payload for provide_performance_feedback task. Returns either success confirmation OR error information, never both.
868
+
869
+ class ProvidePerformanceFeedbackResponseVariant1(BaseModel):
870
+ """Success response - feedback received and processed"""
871
+
872
+ model_config = ConfigDict(extra="forbid")
873
+
874
+ success: Literal[True] = Field(description="Whether the performance feedback was successfully received")
875
+
876
+
877
+ class ProvidePerformanceFeedbackResponseVariant2(BaseModel):
878
+ """Error response - feedback rejected or could not be processed"""
879
+
880
+ model_config = ConfigDict(extra="forbid")
881
+
882
+ errors: list[Error] = Field(description="Array of errors explaining why feedback was rejected (e.g., invalid measurement period, missing campaign data)")
582
883
 
583
- success: bool = Field(description="Whether the performance feedback was successfully received")
584
- errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., invalid measurement period, missing campaign data)")
585
884
 
885
+ # Union type for Provide Performance Feedback Response
886
+ ProvidePerformanceFeedbackResponse = ProvidePerformanceFeedbackResponseVariant1 | ProvidePerformanceFeedbackResponseVariant2
586
887
 
587
- class SyncCreativesResponse(BaseModel):
588
- """Response from creative sync operation with results for each creative"""
589
888
 
590
- dry_run: bool | None = Field(None, description="Whether this was a dry run (no actual changes made)")
591
- creatives: list[dict[str, Any]] = Field(description="Results for each creative processed")
889
+ class TasksGetResponse(BaseModel):
890
+ """Response containing detailed information about a specific task including status and optional conversation history across all AdCP domains"""
592
891
 
892
+ task_id: str = Field(description="Unique identifier for this task")
893
+ task_type: TaskType = Field(description="Type of AdCP operation")
894
+ domain: Literal["media-buy", "signals"] = Field(description="AdCP domain this task belongs to")
895
+ status: TaskStatus = Field(description="Current task status")
896
+ created_at: str = Field(description="When the task was initially created (ISO 8601)")
897
+ updated_at: str = Field(description="When the task was last updated (ISO 8601)")
898
+ completed_at: str | None = Field(None, description="When the task completed (ISO 8601, only for completed/failed/canceled tasks)")
899
+ has_webhook: bool | None = Field(None, description="Whether this task has webhook configuration")
900
+ progress: dict[str, Any] | None = Field(None, description="Progress information for long-running tasks")
901
+ error: dict[str, Any] | None = Field(None, description="Error details for failed tasks")
902
+ history: list[dict[str, Any]] | None = Field(None, description="Complete conversation history for this task (only included if include_history was true in request)")
593
903
 
594
- class UpdateMediaBuyResponse(BaseModel):
595
- """Response payload for update_media_buy task"""
596
904
 
597
- media_buy_id: str = Field(description="Publisher's identifier for the media buy")
598
- buyer_ref: str = Field(description="Buyer's reference identifier for the media buy")
599
- implementation_date: Any | None = Field(None, description="ISO 8601 timestamp when changes take effect (null if pending approval)")
600
- affected_packages: list[dict[str, Any]] | None = Field(None, description="Array of packages that were modified")
601
- errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., partial update failures)")
905
+ class TasksListResponse(BaseModel):
906
+ """Response from task listing query with filtered results and state reconciliation data across all AdCP domains"""
907
+
908
+ query_summary: dict[str, Any] = Field(description="Summary of the query that was executed")
909
+ tasks: list[dict[str, Any]] = Field(description="Array of tasks matching the query criteria")
910
+ pagination: dict[str, Any] = Field(description="Pagination information")
602
911
 
603
912
 
604
913
 
@@ -653,3 +962,211 @@ class PreviewCreativeResponse(BaseModel):
653
962
 
654
963
  # Batch mode field
655
964
  results: list[dict[str, Any]] | None = Field(default=None, description="Array of preview results for batch processing")
965
+
966
+
967
+ # ============================================================================
968
+ # ONEOF DISCRIMINATED UNIONS FOR RESPONSE TYPES
969
+ # ============================================================================
970
+ # These response types use oneOf semantics: success XOR error, never both.
971
+ # Implemented as Union types with distinct Success/Error variants.
972
+
973
+
974
+ class ActivateSignalSuccess(BaseModel):
975
+ """Successful signal activation response"""
976
+
977
+ decisioning_platform_segment_id: str = Field(
978
+ description="The platform-specific ID to use once activated"
979
+ )
980
+ estimated_activation_duration_minutes: float | None = None
981
+ deployed_at: str | None = None
982
+
983
+
984
+ class ActivateSignalError(BaseModel):
985
+ """Failed signal activation response"""
986
+
987
+ errors: list[Error] = Field(description="Task-specific errors and warnings")
988
+
989
+
990
+ # Override the generated ActivateSignalResponse type alias
991
+ ActivateSignalResponse = ActivateSignalSuccess | ActivateSignalError
992
+
993
+
994
+ class CreateMediaBuySuccess(BaseModel):
995
+ """Successful media buy creation response"""
996
+
997
+ media_buy_id: str = Field(description="The unique ID for the media buy")
998
+ buyer_ref: str = Field(description="The buyer's reference ID for this media buy")
999
+ packages: list[Package] = Field(
1000
+ description="Array of approved packages. Each package is ready for creative assignment."
1001
+ )
1002
+ creative_deadline: str | None = Field(
1003
+ None,
1004
+ description="ISO 8601 date when creatives must be provided for launch",
1005
+ )
1006
+
1007
+
1008
+ class CreateMediaBuyError(BaseModel):
1009
+ """Failed media buy creation response"""
1010
+
1011
+ errors: list[Error] = Field(description="Task-specific errors and warnings")
1012
+
1013
+
1014
+ # Override the generated CreateMediaBuyResponse type alias
1015
+ CreateMediaBuyResponse = CreateMediaBuySuccess | CreateMediaBuyError
1016
+
1017
+
1018
+ class UpdateMediaBuySuccess(BaseModel):
1019
+ """Successful media buy update response"""
1020
+
1021
+ media_buy_id: str = Field(description="The unique ID for the media buy")
1022
+ buyer_ref: str = Field(description="The buyer's reference ID for this media buy")
1023
+ packages: list[Package] = Field(
1024
+ description="Array of updated packages reflecting the changes"
1025
+ )
1026
+
1027
+
1028
+ class UpdateMediaBuyError(BaseModel):
1029
+ """Failed media buy update response"""
1030
+
1031
+ errors: list[Error] = Field(description="Task-specific errors and warnings")
1032
+
1033
+
1034
+ # Override the generated UpdateMediaBuyResponse type alias
1035
+ UpdateMediaBuyResponse = UpdateMediaBuySuccess | UpdateMediaBuyError
1036
+
1037
+
1038
+ class SyncCreativesSuccess(BaseModel):
1039
+ """Successful creative sync response"""
1040
+
1041
+ assignments: list[CreativeAssignment] = Field(
1042
+ description="Array of creative assignments with updated status"
1043
+ )
1044
+
1045
+
1046
+ class SyncCreativesError(BaseModel):
1047
+ """Failed creative sync response"""
1048
+
1049
+ errors: list[Error] = Field(description="Task-specific errors and warnings")
1050
+
1051
+
1052
+ # Override the generated SyncCreativesResponse type alias
1053
+ SyncCreativesResponse = SyncCreativesSuccess | SyncCreativesError
1054
+
1055
+
1056
+ # Explicit exports for module interface
1057
+ __all__ = [
1058
+ "ActivateSignalError",
1059
+ "ActivateSignalRequest",
1060
+ "ActivateSignalResponse",
1061
+ "ActivateSignalSuccess",
1062
+ "ActivationKey",
1063
+ "AgentDeployment",
1064
+ "AgentDestination",
1065
+ "BothPreviewRender",
1066
+ "BrandManifest",
1067
+ "BrandManifestRef",
1068
+ "BrandManifestRefVariant1",
1069
+ "BrandManifestRefVariant2",
1070
+ "BuildCreativeRequest",
1071
+ "BuildCreativeResponse",
1072
+ "BuildCreativeResponseVariant1",
1073
+ "BuildCreativeResponseVariant2",
1074
+ "Channels",
1075
+ "CreateMediaBuyError",
1076
+ "CreateMediaBuyRequest",
1077
+ "CreateMediaBuyResponse",
1078
+ "CreateMediaBuySuccess",
1079
+ "CreativeAsset",
1080
+ "CreativeAssignment",
1081
+ "CreativeManifest",
1082
+ "CreativePolicy",
1083
+ "DaastAsset",
1084
+ "DeliveryMetrics",
1085
+ "DeliveryType",
1086
+ "Deployment",
1087
+ "Destination",
1088
+ "Error",
1089
+ "Format",
1090
+ "FormatId",
1091
+ "FrequencyCap",
1092
+ "GetMediaBuyDeliveryRequest",
1093
+ "GetMediaBuyDeliveryResponse",
1094
+ "GetProductsRequest",
1095
+ "GetProductsResponse",
1096
+ "GetSignalsRequest",
1097
+ "GetSignalsResponse",
1098
+ "HtmlPreviewRender",
1099
+ "InlineDaastAsset",
1100
+ "InlineVastAsset",
1101
+ "Key_valueActivationKey",
1102
+ "ListAuthorizedPropertiesRequest",
1103
+ "ListAuthorizedPropertiesResponse",
1104
+ "ListCreativeFormatsRequest",
1105
+ "ListCreativeFormatsResponse",
1106
+ "ListCreativesRequest",
1107
+ "ListCreativesResponse",
1108
+ "Measurement",
1109
+ "MediaBuy",
1110
+ "MediaBuyStatus",
1111
+ "MediaSubAsset",
1112
+ "Pacing",
1113
+ "Package",
1114
+ "PackageRequest",
1115
+ "PackageStatus",
1116
+ "PerformanceFeedback",
1117
+ "Placement",
1118
+ "PlatformDeployment",
1119
+ "PlatformDestination",
1120
+ "PreviewCreativeRequest",
1121
+ "PreviewCreativeResponse",
1122
+ "PreviewRender",
1123
+ "PricingModel",
1124
+ "PricingOption",
1125
+ "PricingOptionVariant1",
1126
+ "PricingOptionVariant2",
1127
+ "PricingOptionVariant3",
1128
+ "PricingOptionVariant4",
1129
+ "PricingOptionVariant5",
1130
+ "PricingOptionVariant6",
1131
+ "PricingOptionVariant7",
1132
+ "PricingOptionVariant8",
1133
+ "PricingOptionVariant9",
1134
+ "Product",
1135
+ "PromotedProducts",
1136
+ "Property",
1137
+ "ProtocolEnvelope",
1138
+ "ProvidePerformanceFeedbackRequest",
1139
+ "ProvidePerformanceFeedbackResponse",
1140
+ "ProvidePerformanceFeedbackResponseVariant1",
1141
+ "ProvidePerformanceFeedbackResponseVariant2",
1142
+ "PushNotificationConfig",
1143
+ "ReportingCapabilities",
1144
+ "Response",
1145
+ "Segment_idActivationKey",
1146
+ "StandardFormatIds",
1147
+ "StartTiming",
1148
+ "StartTimingVariant1",
1149
+ "StartTimingVariant2",
1150
+ "SubAsset",
1151
+ "SyncCreativesError",
1152
+ "SyncCreativesRequest",
1153
+ "SyncCreativesResponse",
1154
+ "SyncCreativesSuccess",
1155
+ "Targeting",
1156
+ "TaskStatus",
1157
+ "TaskType",
1158
+ "TasksGetRequest",
1159
+ "TasksGetResponse",
1160
+ "TasksListRequest",
1161
+ "TasksListResponse",
1162
+ "TextSubAsset",
1163
+ "UpdateMediaBuyError",
1164
+ "UpdateMediaBuyRequest",
1165
+ "UpdateMediaBuyResponse",
1166
+ "UpdateMediaBuySuccess",
1167
+ "UrlDaastAsset",
1168
+ "UrlPreviewRender",
1169
+ "UrlVastAsset",
1170
+ "VastAsset",
1171
+ "WebhookPayload",
1172
+ ]
@@ -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.1.0
3
+ Version: 1.2.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
@@ -59,6 +59,8 @@ 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
+
62
64
  ## Quick Start: Distributed Operations
63
65
 
64
66
  ```python
@@ -1,23 +1,23 @@
1
- adcp/__init__.py,sha256=p3emIEsC2FN_jcnHOP62BRPs6h4lgIhFselvAlTM0LI,2512
1
+ adcp/__init__.py,sha256=3UpoB71XItkSZ_r1QuzE2GZIQsgVjt50ZawxQUO2-qs,6232
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
10
  adcp/types/__init__.py,sha256=3E_TJUXqQQFcjmSZZSPLwqBP3s_ijsH2LDeuOU-MP30,402
11
11
  adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
12
- adcp/types/generated.py,sha256=j21CgpQExfd2gZTEnDUlVO3hvBmdn-4yBzgC86GUEnI,52485
12
+ adcp/types/generated.py,sha256=Ig4ucbJzKRuHlwYzsqvMF9M3w2KghhQQqsXuOnBqVMM,74993
13
13
  adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
14
14
  adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
15
15
  adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
16
16
  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.1.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
19
- adcp-1.1.0.dist-info/METADATA,sha256=51wZBtGtYyiqVRSaZtPW3OqvQOcZurM-A2FUvqRgrV8,14455
20
- adcp-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- adcp-1.1.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
22
- adcp-1.1.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
23
- adcp-1.1.0.dist-info/RECORD,,
17
+ adcp/utils/response_parser.py,sha256=uPk2vIH-RYZmq7y3i8lC4HTMQ3FfKdlgXKTjgJ1955M,6253
18
+ adcp-1.2.1.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
19
+ adcp-1.2.1.dist-info/METADATA,sha256=mDI2lRdXxOluUZ9vgRE3fyoGSnj8DQ8nqBU30Tuphjg,14568
20
+ adcp-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ adcp-1.2.1.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
22
+ adcp-1.2.1.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
23
+ adcp-1.2.1.dist-info/RECORD,,
File without changes