adcp 2.16.0__py3-none-any.whl → 2.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. adcp/ADCP_VERSION +1 -1
  2. adcp/__init__.py +22 -1
  3. adcp/__main__.py +72 -0
  4. adcp/types/_generated.py +7 -1
  5. adcp/types/generated_poc/adagents.py +14 -14
  6. adcp/types/generated_poc/core/activation_key.py +3 -3
  7. adcp/types/generated_poc/core/assets/audio_asset.py +2 -2
  8. adcp/types/generated_poc/core/assets/css_asset.py +2 -2
  9. adcp/types/generated_poc/core/assets/daast_asset.py +3 -3
  10. adcp/types/generated_poc/core/assets/html_asset.py +2 -2
  11. adcp/types/generated_poc/core/assets/image_asset.py +2 -2
  12. adcp/types/generated_poc/core/assets/javascript_asset.py +2 -2
  13. adcp/types/generated_poc/core/assets/text_asset.py +2 -2
  14. adcp/types/generated_poc/core/assets/url_asset.py +2 -2
  15. adcp/types/generated_poc/core/assets/vast_asset.py +3 -3
  16. adcp/types/generated_poc/core/assets/video_asset.py +2 -2
  17. adcp/types/generated_poc/core/assets/webhook_asset.py +2 -2
  18. adcp/types/generated_poc/core/brand_manifest.py +4 -4
  19. adcp/types/generated_poc/core/context.py +1 -2
  20. adcp/types/generated_poc/core/creative_asset.py +3 -3
  21. adcp/types/generated_poc/core/creative_assignment.py +2 -2
  22. adcp/types/generated_poc/core/creative_filters.py +2 -2
  23. adcp/types/generated_poc/core/creative_manifest.py +2 -2
  24. adcp/types/generated_poc/core/creative_policy.py +2 -2
  25. adcp/types/generated_poc/core/delivery_metrics.py +3 -3
  26. adcp/types/generated_poc/core/deployment.py +3 -3
  27. adcp/types/generated_poc/core/destination.py +3 -3
  28. adcp/types/generated_poc/core/error.py +5 -5
  29. adcp/types/generated_poc/core/ext.py +1 -2
  30. adcp/types/generated_poc/core/format.py +94 -7
  31. adcp/types/generated_poc/core/format_id.py +2 -2
  32. adcp/types/generated_poc/core/frequency_cap.py +2 -2
  33. adcp/types/generated_poc/core/measurement.py +2 -2
  34. adcp/types/generated_poc/core/media_buy.py +2 -2
  35. adcp/types/generated_poc/core/package.py +2 -2
  36. adcp/types/generated_poc/core/performance_feedback.py +3 -3
  37. adcp/types/generated_poc/core/placement.py +2 -2
  38. adcp/types/generated_poc/core/product.py +4 -4
  39. adcp/types/generated_poc/core/product_filters.py +4 -4
  40. adcp/types/generated_poc/core/promoted_offerings.py +4 -4
  41. adcp/types/generated_poc/core/promoted_products.py +2 -2
  42. adcp/types/generated_poc/core/property.py +3 -3
  43. adcp/types/generated_poc/core/protocol_envelope.py +2 -2
  44. adcp/types/generated_poc/core/publisher_property_selector.py +4 -4
  45. adcp/types/generated_poc/core/reporting_capabilities.py +2 -2
  46. adcp/types/generated_poc/core/response.py +2 -2
  47. adcp/types/generated_poc/core/signal_filters.py +2 -2
  48. adcp/types/generated_poc/core/sub_asset.py +3 -3
  49. adcp/types/generated_poc/core/targeting.py +2 -2
  50. adcp/types/generated_poc/creative/list_creative_formats_request.py +2 -2
  51. adcp/types/generated_poc/creative/list_creative_formats_response.py +2 -2
  52. adcp/types/generated_poc/creative/preview_creative_request.py +7 -7
  53. adcp/types/generated_poc/creative/preview_creative_response.py +3 -3
  54. adcp/types/generated_poc/creative/preview_render.py +4 -4
  55. adcp/types/generated_poc/media_buy/build_creative_request.py +2 -2
  56. adcp/types/generated_poc/media_buy/build_creative_response.py +3 -3
  57. adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py +2 -2
  58. adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py +2 -2
  59. adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py +2 -2
  60. adcp/types/generated_poc/media_buy/create_media_buy_request.py +47 -6
  61. adcp/types/generated_poc/media_buy/create_media_buy_response.py +3 -3
  62. adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +2 -2
  63. adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +6 -6
  64. adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py +2 -2
  65. adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py +2 -2
  66. adcp/types/generated_poc/media_buy/get_products_async_response_working.py +2 -2
  67. adcp/types/generated_poc/media_buy/get_products_request.py +2 -2
  68. adcp/types/generated_poc/media_buy/get_products_response.py +2 -2
  69. adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +2 -2
  70. adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +2 -2
  71. adcp/types/generated_poc/media_buy/list_creative_formats_request.py +2 -2
  72. adcp/types/generated_poc/media_buy/list_creative_formats_response.py +2 -2
  73. adcp/types/generated_poc/media_buy/list_creatives_request.py +4 -4
  74. adcp/types/generated_poc/media_buy/list_creatives_response.py +9 -9
  75. adcp/types/generated_poc/media_buy/package_request.py +2 -2
  76. adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +4 -4
  77. adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +3 -3
  78. adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py +2 -2
  79. adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py +2 -2
  80. adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py +2 -2
  81. adcp/types/generated_poc/media_buy/sync_creatives_request.py +2 -2
  82. adcp/types/generated_poc/media_buy/sync_creatives_response.py +4 -4
  83. adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py +2 -2
  84. adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py +2 -2
  85. adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py +2 -2
  86. adcp/types/generated_poc/media_buy/update_media_buy_request.py +5 -5
  87. adcp/types/generated_poc/media_buy/update_media_buy_response.py +3 -3
  88. adcp/types/generated_poc/pricing_options/cpc_option.py +2 -2
  89. adcp/types/generated_poc/pricing_options/cpcv_option.py +2 -2
  90. adcp/types/generated_poc/pricing_options/cpm_auction_option.py +2 -2
  91. adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +2 -2
  92. adcp/types/generated_poc/pricing_options/cpp_option.py +3 -3
  93. adcp/types/generated_poc/pricing_options/cpv_option.py +4 -4
  94. adcp/types/generated_poc/pricing_options/flat_rate_option.py +3 -3
  95. adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +2 -2
  96. adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +2 -2
  97. adcp/types/generated_poc/protocols/adcp_extension.py +2 -2
  98. adcp/types/generated_poc/signals/activate_signal_request.py +2 -2
  99. adcp/types/generated_poc/signals/activate_signal_response.py +3 -3
  100. adcp/types/generated_poc/signals/get_signals_request.py +3 -3
  101. adcp/types/generated_poc/signals/get_signals_response.py +4 -4
  102. adcp/utils/__init__.py +24 -1
  103. adcp/utils/format_assets.py +224 -0
  104. adcp/utils/preview_cache.py +29 -7
  105. adcp/utils/response_parser.py +81 -6
  106. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/METADATA +1 -1
  107. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/RECORD +111 -110
  108. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/WHEEL +0 -0
  109. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/entry_points.txt +0 -0
  110. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/licenses/LICENSE +0 -0
  111. {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/activate_signal_response.json
3
- # timestamp: 2025-12-11T15:09:37+00:00
3
+ # timestamp: 2026-01-08T19:25:24+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -16,7 +16,7 @@ from ..core import ext as ext_1
16
16
 
17
17
  class ActivateSignalResponse1(AdCPBaseModel):
18
18
  model_config = ConfigDict(
19
- extra='forbid',
19
+ extra='allow',
20
20
  )
21
21
  context: context_1.ContextObject | None = None
22
22
  deployments: Annotated[
@@ -28,7 +28,7 @@ class ActivateSignalResponse1(AdCPBaseModel):
28
28
 
29
29
  class ActivateSignalResponse2(AdCPBaseModel):
30
30
  model_config = ConfigDict(
31
- extra='forbid',
31
+ extra='allow',
32
32
  )
33
33
  context: context_1.ContextObject | None = None
34
34
  errors: Annotated[
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/get_signals_request.json
3
- # timestamp: 2025-12-11T15:09:37+00:00
3
+ # timestamp: 2026-01-08T19:25:24+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -21,7 +21,7 @@ class Country(RootModel[str]):
21
21
 
22
22
  class DeliverTo(AdCPBaseModel):
23
23
  model_config = ConfigDict(
24
- extra='forbid',
24
+ extra='allow',
25
25
  )
26
26
  countries: Annotated[
27
27
  list[Country], Field(description='Countries where signals will be used (ISO codes)')
@@ -37,7 +37,7 @@ class DeliverTo(AdCPBaseModel):
37
37
 
38
38
  class GetSignalsRequest(AdCPBaseModel):
39
39
  model_config = ConfigDict(
40
- extra='forbid',
40
+ extra='allow',
41
41
  )
42
42
  context: context_1.ContextObject | None = None
43
43
  deliver_to: Annotated[
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/get_signals_response.json
3
- # timestamp: 2025-12-11T15:09:37+00:00
3
+ # timestamp: 2026-01-08T19:25:24+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -17,7 +17,7 @@ from ..enums import signal_catalog_type
17
17
 
18
18
  class Pricing(AdCPBaseModel):
19
19
  model_config = ConfigDict(
20
- extra='forbid',
20
+ extra='allow',
21
21
  )
22
22
  cpm: Annotated[float, Field(description='Cost per thousand impressions', ge=0.0)]
23
23
  currency: Annotated[str, Field(description='Currency code', pattern='^[A-Z]{3}$')]
@@ -25,7 +25,7 @@ class Pricing(AdCPBaseModel):
25
25
 
26
26
  class Signal(AdCPBaseModel):
27
27
  model_config = ConfigDict(
28
- extra='forbid',
28
+ extra='allow',
29
29
  )
30
30
  coverage_percentage: Annotated[
31
31
  float, Field(description='Percentage of audience coverage', ge=0.0, le=100.0)
@@ -45,7 +45,7 @@ class Signal(AdCPBaseModel):
45
45
 
46
46
  class GetSignalsResponse(AdCPBaseModel):
47
47
  model_config = ConfigDict(
48
- extra='forbid',
48
+ extra='allow',
49
49
  )
50
50
  context: context_1.ContextObject | None = None
51
51
  errors: Annotated[
adcp/utils/__init__.py CHANGED
@@ -2,6 +2,29 @@ from __future__ import annotations
2
2
 
3
3
  """Utility functions."""
4
4
 
5
+ from adcp.utils.format_assets import (
6
+ get_asset_count,
7
+ get_format_assets,
8
+ get_individual_assets,
9
+ get_optional_assets,
10
+ get_repeatable_groups,
11
+ get_required_assets,
12
+ has_assets,
13
+ normalize_assets_required,
14
+ uses_deprecated_assets_field,
15
+ )
5
16
  from adcp.utils.operation_id import create_operation_id
6
17
 
7
- __all__ = ["create_operation_id"]
18
+ __all__ = [
19
+ "create_operation_id",
20
+ # Format asset utilities
21
+ "get_format_assets",
22
+ "normalize_assets_required",
23
+ "get_required_assets",
24
+ "get_optional_assets",
25
+ "get_individual_assets",
26
+ "get_repeatable_groups",
27
+ "uses_deprecated_assets_field",
28
+ "get_asset_count",
29
+ "has_assets",
30
+ ]
@@ -0,0 +1,224 @@
1
+ """Format Asset Utilities.
2
+
3
+ Provides backward-compatible access to format assets.
4
+ The v2.6 `assets` field replaces the deprecated `assets_required` field.
5
+
6
+ These utilities help users work with format assets regardless of which field
7
+ the agent uses, providing a smooth migration path.
8
+
9
+ Example:
10
+ ```python
11
+ from adcp import Format
12
+ from adcp.utils.format_assets import get_format_assets, get_required_assets
13
+
14
+ # Get all assets from a format (handles both new and deprecated fields)
15
+ all_assets = get_format_assets(format)
16
+
17
+ # Get only required assets
18
+ required = get_required_assets(format)
19
+
20
+ # Check if using deprecated field
21
+ if uses_deprecated_assets_field(format):
22
+ print("Agent should migrate to 'assets' field")
23
+ ```
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import TYPE_CHECKING, Any, Union
29
+
30
+ from adcp.types.generated_poc.core.format import Assets as AssetsModel
31
+ from adcp.types.generated_poc.core.format import Assets1 as Assets1Model
32
+
33
+ if TYPE_CHECKING:
34
+ from adcp.types.generated_poc.core.format import Assets, Assets1, Format
35
+
36
+ # Type alias for any format asset (individual or repeatable group)
37
+ FormatAsset = Union["Assets", "Assets1"]
38
+
39
+
40
+ def get_format_assets(format: Format) -> list[FormatAsset]:
41
+ """Get assets from a Format, preferring new `assets` field, falling back to `assets_required`.
42
+
43
+ This provides backward compatibility during the migration from `assets_required` to `assets`.
44
+ - If `assets` exists and has items, returns it directly
45
+ - If only `assets_required` exists, normalizes it to the new format (sets required=True)
46
+ - Returns empty list if neither field exists (flexible format with no assets)
47
+
48
+ Args:
49
+ format: The Format object from list_creative_formats response
50
+
51
+ Returns:
52
+ List of assets in the new format structure
53
+
54
+ Example:
55
+ ```python
56
+ formats = await agent.simple.list_creative_formats()
57
+ for format in formats.formats:
58
+ assets = get_format_assets(format)
59
+ print(f"{format.name} has {len(assets)} assets")
60
+ ```
61
+ """
62
+ # Prefer new `assets` field (v2.6+)
63
+ if format.assets and len(format.assets) > 0:
64
+ return list(format.assets)
65
+
66
+ # Fall back to deprecated `assets_required` and normalize
67
+ if format.assets_required and len(format.assets_required) > 0:
68
+ return normalize_assets_required(format.assets_required)
69
+
70
+ return []
71
+
72
+
73
+ def normalize_assets_required(assets_required: list[Any]) -> list[FormatAsset]:
74
+ """Convert deprecated assets_required to new assets format.
75
+
76
+ All assets in assets_required are required by definition (that's why they were in
77
+ that array). The new `assets` field has an explicit `required: boolean` to allow
78
+ both required AND optional assets.
79
+
80
+ Args:
81
+ assets_required: The deprecated assets_required array
82
+
83
+ Returns:
84
+ Normalized assets as Pydantic models with explicit required=True
85
+ """
86
+ normalized: list[FormatAsset] = []
87
+ for asset in assets_required:
88
+ # Get asset data as dict
89
+ if isinstance(asset, dict):
90
+ asset_dict = asset
91
+ else:
92
+ asset_dict = asset.model_dump() if hasattr(asset, "model_dump") else dict(asset)
93
+
94
+ # Check if it's a repeatable group (has asset_group_id) or individual asset
95
+ if "asset_group_id" in asset_dict:
96
+ # Repeatable group - use Assets1Model
97
+ normalized.append(Assets1Model(**{**asset_dict, "required": True}))
98
+ else:
99
+ # Individual asset - use AssetsModel
100
+ normalized.append(AssetsModel(**{**asset_dict, "required": True}))
101
+
102
+ return normalized
103
+
104
+
105
+ def get_required_assets(format: Format) -> list[FormatAsset]:
106
+ """Get only required assets from a Format.
107
+
108
+ Args:
109
+ format: The Format object
110
+
111
+ Returns:
112
+ List of required assets only
113
+
114
+ Example:
115
+ ```python
116
+ required_assets = get_required_assets(format)
117
+ print(f"Must provide {len(required_assets)} assets")
118
+ ```
119
+ """
120
+ return [asset for asset in get_format_assets(format) if _is_required(asset)]
121
+
122
+
123
+ def get_optional_assets(format: Format) -> list[FormatAsset]:
124
+ """Get only optional assets from a Format.
125
+
126
+ Note: When using deprecated `assets_required`, this will always return empty
127
+ since assets_required only contained required assets.
128
+
129
+ Args:
130
+ format: The Format object
131
+
132
+ Returns:
133
+ List of optional assets only
134
+
135
+ Example:
136
+ ```python
137
+ optional_assets = get_optional_assets(format)
138
+ print(f"Can optionally provide {len(optional_assets)} additional assets")
139
+ ```
140
+ """
141
+ return [asset for asset in get_format_assets(format) if not _is_required(asset)]
142
+
143
+
144
+ def get_individual_assets(format: Format) -> list[FormatAsset]:
145
+ """Get individual assets (not repeatable groups) from a Format.
146
+
147
+ Args:
148
+ format: The Format object
149
+
150
+ Returns:
151
+ List of individual assets (item_type='individual')
152
+ """
153
+ return [asset for asset in get_format_assets(format) if _get_item_type(asset) == "individual"]
154
+
155
+
156
+ def get_repeatable_groups(format: Format) -> list[FormatAsset]:
157
+ """Get repeatable asset groups from a Format.
158
+
159
+ Args:
160
+ format: The Format object
161
+
162
+ Returns:
163
+ List of repeatable asset groups (item_type='repeatable_group')
164
+ """
165
+ return [
166
+ asset for asset in get_format_assets(format) if _get_item_type(asset) == "repeatable_group"
167
+ ]
168
+
169
+
170
+ def uses_deprecated_assets_field(format: Format) -> bool:
171
+ """Check if format uses deprecated assets_required field (for migration warnings).
172
+
173
+ Args:
174
+ format: The Format object
175
+
176
+ Returns:
177
+ True if using deprecated field, False if using new field or neither
178
+
179
+ Example:
180
+ ```python
181
+ if uses_deprecated_assets_field(format):
182
+ print(f"Format {format.name} uses deprecated assets_required field")
183
+ ```
184
+ """
185
+ has_assets = format.assets is not None and len(format.assets) > 0
186
+ has_assets_required = format.assets_required is not None and len(format.assets_required) > 0
187
+ return not has_assets and has_assets_required
188
+
189
+
190
+ def get_asset_count(format: Format) -> int:
191
+ """Get the count of assets in a format (for display purposes).
192
+
193
+ Args:
194
+ format: The Format object
195
+
196
+ Returns:
197
+ Number of assets, or 0 if none defined
198
+ """
199
+ return len(get_format_assets(format))
200
+
201
+
202
+ def has_assets(format: Format) -> bool:
203
+ """Check if a format has any assets defined.
204
+
205
+ Args:
206
+ format: The Format object
207
+
208
+ Returns:
209
+ True if format has assets, False otherwise
210
+ """
211
+ return get_asset_count(format) > 0
212
+
213
+
214
+ # Internal helpers
215
+
216
+
217
+ def _is_required(asset: FormatAsset) -> bool:
218
+ """Check if an asset is required."""
219
+ return getattr(asset, "required", False) is True
220
+
221
+
222
+ def _get_item_type(asset: FormatAsset) -> str:
223
+ """Get the item_type of an asset."""
224
+ return getattr(asset, "item_type", "individual")
@@ -399,25 +399,47 @@ def _create_sample_manifest_for_format(fmt: Format) -> CreativeManifest | None:
399
399
  Sample CreativeManifest, or None if unable to create one
400
400
  """
401
401
  from adcp.types import CreativeManifest
402
+ from adcp.utils.format_assets import get_required_assets
402
403
 
403
- if not fmt.assets_required:
404
+ required_assets = get_required_assets(fmt)
405
+ if not required_assets:
404
406
  return None
405
407
 
406
408
  assets: dict[str, Any] = {}
407
409
 
408
- for asset in fmt.assets_required:
410
+ for asset in required_assets:
409
411
  if isinstance(asset, dict):
412
+ # Handle dict input
410
413
  asset_id = asset.get("asset_id")
411
414
  asset_type = asset.get("asset_type")
412
415
 
413
416
  if asset_id:
414
417
  assets[asset_id] = _create_sample_asset(asset_type)
415
418
  else:
416
- # Handle Pydantic model
417
- asset_id = asset.asset_id
418
- has_value = hasattr(asset.asset_type, "value")
419
- asset_type = asset.asset_type.value if has_value else str(asset.asset_type)
420
- assets[asset_id] = _create_sample_asset(asset_type)
419
+ # Handle Pydantic model - check for individual vs repeatable_group
420
+ item_type = getattr(asset, "item_type", "individual")
421
+
422
+ if item_type == "individual":
423
+ asset_id = asset.asset_id
424
+ has_value = hasattr(asset.asset_type, "value")
425
+ asset_type = asset.asset_type.value if has_value else str(asset.asset_type)
426
+ assets[asset_id] = _create_sample_asset(asset_type)
427
+ elif item_type == "repeatable_group":
428
+ # For repeatable groups, create sample assets for each asset in the group
429
+ group_assets = getattr(asset, "assets", [])
430
+ for group_asset in group_assets:
431
+ if isinstance(group_asset, dict):
432
+ asset_id = group_asset.get("asset_id")
433
+ asset_type = group_asset.get("asset_type")
434
+ else:
435
+ asset_id = group_asset.asset_id
436
+ if hasattr(group_asset.asset_type, "value"):
437
+ asset_type = group_asset.asset_type.value
438
+ else:
439
+ asset_type = str(group_asset.asset_type)
440
+
441
+ if asset_id:
442
+ assets[asset_id] = _create_sample_asset(asset_type)
421
443
 
422
444
  if not assets:
423
445
  return None
@@ -129,12 +129,67 @@ def parse_mcp_content(content: list[dict[str, Any]], response_type: type[T]) ->
129
129
  )
130
130
 
131
131
 
132
+ # Protocol-level fields from ProtocolResponse (core/response.json) and
133
+ # ProtocolEnvelope (core/protocol_envelope.json). These are separated from
134
+ # task data for schema validation, but preserved at the TaskResult level.
135
+ # Note: 'data' and 'payload' are handled separately as wrapper fields.
136
+ PROTOCOL_FIELDS = {
137
+ "message", # Human-readable summary
138
+ "context_id", # Session continuity identifier
139
+ "task_id", # Async operation identifier
140
+ "status", # Task execution state
141
+ "timestamp", # Response timestamp
142
+ }
143
+
144
+
145
+ def _extract_task_data(data: dict[str, Any]) -> dict[str, Any]:
146
+ """
147
+ Extract task-specific data from a protocol response.
148
+
149
+ Servers may return responses in ProtocolResponse format:
150
+ {"message": "...", "context_id": "...", "data": {...}}
151
+
152
+ Or ProtocolEnvelope format:
153
+ {"message": "...", "status": "...", "payload": {...}}
154
+
155
+ Or task data directly with protocol fields mixed in:
156
+ {"message": "...", "products": [...], ...}
157
+
158
+ This function separates task-specific data for schema validation.
159
+ Protocol fields are preserved at the TaskResult level.
160
+
161
+ Args:
162
+ data: Response data dict
163
+
164
+ Returns:
165
+ Task-specific data suitable for schema validation.
166
+ Returns the same dict object if no extraction is needed.
167
+ """
168
+ # Check for wrapped payload fields
169
+ # (ProtocolResponse uses 'data', ProtocolEnvelope uses 'payload')
170
+ if "data" in data and isinstance(data["data"], dict):
171
+ return data["data"]
172
+ if "payload" in data and isinstance(data["payload"], dict):
173
+ return data["payload"]
174
+
175
+ # Check if any protocol fields are present
176
+ if not any(k in PROTOCOL_FIELDS for k in data):
177
+ return data # Return same object for identity check
178
+
179
+ # Separate task data from protocol fields
180
+ return {k: v for k, v in data.items() if k not in PROTOCOL_FIELDS}
181
+
182
+
132
183
  def parse_json_or_text(data: Any, response_type: type[T]) -> T:
133
184
  """
134
185
  Parse data that might be JSON string, dict, or other format.
135
186
 
136
187
  Used by A2A adapter for flexible response parsing.
137
188
 
189
+ Handles protocol-level wrapping where servers return:
190
+ - {"message": "...", "data": {...task_data...}}
191
+ - {"message": "...", ...task_fields...}
192
+
138
193
  Args:
139
194
  data: Response data (string, dict, or other)
140
195
  response_type: Expected Pydantic model type
@@ -147,22 +202,42 @@ def parse_json_or_text(data: Any, response_type: type[T]) -> T:
147
202
  """
148
203
  # If already a dict, try direct validation
149
204
  if isinstance(data, dict):
205
+ # Try direct validation first
206
+ original_error: Exception | None = None
150
207
  try:
151
208
  return _validate_union_type(data, response_type)
152
- except ValidationError as 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
209
+ except (ValidationError, ValueError) as e:
210
+ original_error = e
211
+
212
+ # Try extracting task data (separates protocol fields)
213
+ task_data = _extract_task_data(data)
214
+ if task_data is not data:
215
+ try:
216
+ return _validate_union_type(task_data, response_type)
217
+ except (ValidationError, ValueError):
218
+ pass # Fall through to raise original error
219
+
220
+ # Report the original validation error
221
+ type_name = getattr(response_type, "__name__", str(response_type))
222
+ raise ValueError(
223
+ f"Response doesn't match expected schema {type_name}: {original_error}"
224
+ ) from original_error
156
225
 
157
226
  # If string, try JSON parsing
158
227
  if isinstance(data, str):
159
228
  try:
160
229
  parsed = json.loads(data)
161
- return _validate_union_type(parsed, response_type)
162
230
  except json.JSONDecodeError as e:
163
231
  raise ValueError(f"Response is not valid JSON: {e}") from e
232
+
233
+ # Recursively handle dict parsing (which includes protocol field extraction)
234
+ if isinstance(parsed, dict):
235
+ return parse_json_or_text(parsed, response_type)
236
+
237
+ # Non-dict JSON (shouldn't happen for AdCP responses)
238
+ try:
239
+ return _validate_union_type(parsed, response_type)
164
240
  except ValidationError as e:
165
- # Get the type name, handling Union types
166
241
  type_name = getattr(response_type, "__name__", str(response_type))
167
242
  raise ValueError(f"Response doesn't match expected schema {type_name}: {e}") from e
168
243
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 2.16.0
3
+ Version: 2.18.0
4
4
  Summary: Official Python client for the Ad Context Protocol (AdCP)
5
5
  Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
6
  License: Apache-2.0