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.
- adcp/ADCP_VERSION +1 -1
- adcp/__init__.py +22 -1
- adcp/__main__.py +72 -0
- adcp/types/_generated.py +7 -1
- adcp/types/generated_poc/adagents.py +14 -14
- adcp/types/generated_poc/core/activation_key.py +3 -3
- adcp/types/generated_poc/core/assets/audio_asset.py +2 -2
- adcp/types/generated_poc/core/assets/css_asset.py +2 -2
- adcp/types/generated_poc/core/assets/daast_asset.py +3 -3
- adcp/types/generated_poc/core/assets/html_asset.py +2 -2
- adcp/types/generated_poc/core/assets/image_asset.py +2 -2
- adcp/types/generated_poc/core/assets/javascript_asset.py +2 -2
- adcp/types/generated_poc/core/assets/text_asset.py +2 -2
- adcp/types/generated_poc/core/assets/url_asset.py +2 -2
- adcp/types/generated_poc/core/assets/vast_asset.py +3 -3
- adcp/types/generated_poc/core/assets/video_asset.py +2 -2
- adcp/types/generated_poc/core/assets/webhook_asset.py +2 -2
- adcp/types/generated_poc/core/brand_manifest.py +4 -4
- adcp/types/generated_poc/core/context.py +1 -2
- adcp/types/generated_poc/core/creative_asset.py +3 -3
- adcp/types/generated_poc/core/creative_assignment.py +2 -2
- adcp/types/generated_poc/core/creative_filters.py +2 -2
- adcp/types/generated_poc/core/creative_manifest.py +2 -2
- adcp/types/generated_poc/core/creative_policy.py +2 -2
- adcp/types/generated_poc/core/delivery_metrics.py +3 -3
- adcp/types/generated_poc/core/deployment.py +3 -3
- adcp/types/generated_poc/core/destination.py +3 -3
- adcp/types/generated_poc/core/error.py +5 -5
- adcp/types/generated_poc/core/ext.py +1 -2
- adcp/types/generated_poc/core/format.py +94 -7
- adcp/types/generated_poc/core/format_id.py +2 -2
- adcp/types/generated_poc/core/frequency_cap.py +2 -2
- adcp/types/generated_poc/core/measurement.py +2 -2
- adcp/types/generated_poc/core/media_buy.py +2 -2
- adcp/types/generated_poc/core/package.py +2 -2
- adcp/types/generated_poc/core/performance_feedback.py +3 -3
- adcp/types/generated_poc/core/placement.py +2 -2
- adcp/types/generated_poc/core/product.py +4 -4
- adcp/types/generated_poc/core/product_filters.py +4 -4
- adcp/types/generated_poc/core/promoted_offerings.py +4 -4
- adcp/types/generated_poc/core/promoted_products.py +2 -2
- adcp/types/generated_poc/core/property.py +3 -3
- adcp/types/generated_poc/core/protocol_envelope.py +2 -2
- adcp/types/generated_poc/core/publisher_property_selector.py +4 -4
- adcp/types/generated_poc/core/reporting_capabilities.py +2 -2
- adcp/types/generated_poc/core/response.py +2 -2
- adcp/types/generated_poc/core/signal_filters.py +2 -2
- adcp/types/generated_poc/core/sub_asset.py +3 -3
- adcp/types/generated_poc/core/targeting.py +2 -2
- adcp/types/generated_poc/creative/list_creative_formats_request.py +2 -2
- adcp/types/generated_poc/creative/list_creative_formats_response.py +2 -2
- adcp/types/generated_poc/creative/preview_creative_request.py +7 -7
- adcp/types/generated_poc/creative/preview_creative_response.py +3 -3
- adcp/types/generated_poc/creative/preview_render.py +4 -4
- adcp/types/generated_poc/media_buy/build_creative_request.py +2 -2
- adcp/types/generated_poc/media_buy/build_creative_response.py +3 -3
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py +2 -2
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py +2 -2
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py +2 -2
- adcp/types/generated_poc/media_buy/create_media_buy_request.py +47 -6
- adcp/types/generated_poc/media_buy/create_media_buy_response.py +3 -3
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +2 -2
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +6 -6
- adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py +2 -2
- adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py +2 -2
- adcp/types/generated_poc/media_buy/get_products_async_response_working.py +2 -2
- adcp/types/generated_poc/media_buy/get_products_request.py +2 -2
- adcp/types/generated_poc/media_buy/get_products_response.py +2 -2
- adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +2 -2
- adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +2 -2
- adcp/types/generated_poc/media_buy/list_creative_formats_request.py +2 -2
- adcp/types/generated_poc/media_buy/list_creative_formats_response.py +2 -2
- adcp/types/generated_poc/media_buy/list_creatives_request.py +4 -4
- adcp/types/generated_poc/media_buy/list_creatives_response.py +9 -9
- adcp/types/generated_poc/media_buy/package_request.py +2 -2
- adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +4 -4
- adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +3 -3
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py +2 -2
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py +2 -2
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py +2 -2
- adcp/types/generated_poc/media_buy/sync_creatives_request.py +2 -2
- adcp/types/generated_poc/media_buy/sync_creatives_response.py +4 -4
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py +2 -2
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py +2 -2
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py +2 -2
- adcp/types/generated_poc/media_buy/update_media_buy_request.py +5 -5
- adcp/types/generated_poc/media_buy/update_media_buy_response.py +3 -3
- adcp/types/generated_poc/pricing_options/cpc_option.py +2 -2
- adcp/types/generated_poc/pricing_options/cpcv_option.py +2 -2
- adcp/types/generated_poc/pricing_options/cpm_auction_option.py +2 -2
- adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +2 -2
- adcp/types/generated_poc/pricing_options/cpp_option.py +3 -3
- adcp/types/generated_poc/pricing_options/cpv_option.py +4 -4
- adcp/types/generated_poc/pricing_options/flat_rate_option.py +3 -3
- adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +2 -2
- adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +2 -2
- adcp/types/generated_poc/protocols/adcp_extension.py +2 -2
- adcp/types/generated_poc/signals/activate_signal_request.py +2 -2
- adcp/types/generated_poc/signals/activate_signal_response.py +3 -3
- adcp/types/generated_poc/signals/get_signals_request.py +3 -3
- adcp/types/generated_poc/signals/get_signals_response.py +4 -4
- adcp/utils/__init__.py +24 -1
- adcp/utils/format_assets.py +224 -0
- adcp/utils/preview_cache.py +29 -7
- adcp/utils/response_parser.py +81 -6
- {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/METADATA +1 -1
- {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/RECORD +111 -110
- {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/WHEEL +0 -0
- {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/entry_points.txt +0 -0
- {adcp-2.16.0.dist-info → adcp-2.18.0.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
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='
|
|
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='
|
|
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:
|
|
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='
|
|
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='
|
|
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:
|
|
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='
|
|
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='
|
|
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='
|
|
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__ = [
|
|
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")
|
adcp/utils/preview_cache.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
adcp/utils/response_parser.py
CHANGED
|
@@ -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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|