adcp 2.12.2__py3-none-any.whl → 2.14.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 (62) hide show
  1. adcp/__init__.py +14 -1
  2. adcp/adagents.py +53 -9
  3. adcp/client.py +361 -57
  4. adcp/protocols/mcp.py +1 -3
  5. adcp/types/__init__.py +9 -33
  6. adcp/types/_generated.py +82 -13
  7. adcp/types/aliases.py +23 -0
  8. adcp/types/base.py +193 -0
  9. adcp/types/generated_poc/adagents.py +9 -13
  10. adcp/types/generated_poc/core/activation_key.py +12 -2
  11. adcp/types/generated_poc/core/assets/daast_asset.py +12 -2
  12. adcp/types/generated_poc/core/assets/image_asset.py +9 -5
  13. adcp/types/generated_poc/core/assets/vast_asset.py +12 -2
  14. adcp/types/generated_poc/core/assets/video_asset.py +9 -5
  15. adcp/types/generated_poc/core/async_response_data.py +72 -0
  16. adcp/types/generated_poc/core/brand_manifest_ref.py +35 -0
  17. adcp/types/generated_poc/core/creative_asset.py +4 -6
  18. adcp/types/generated_poc/core/creative_manifest.py +4 -6
  19. adcp/types/generated_poc/core/deployment.py +16 -8
  20. adcp/types/generated_poc/core/destination.py +12 -2
  21. adcp/types/generated_poc/core/format.py +3 -3
  22. adcp/types/generated_poc/core/{webhook_payload.py → mcp_webhook_payload.py} +8 -33
  23. adcp/types/generated_poc/core/pricing_option.py +51 -0
  24. adcp/types/generated_poc/core/product.py +4 -29
  25. adcp/types/generated_poc/core/promoted_offerings.py +5 -21
  26. adcp/types/generated_poc/core/property.py +4 -6
  27. adcp/types/generated_poc/core/publisher_property_selector.py +15 -2
  28. adcp/types/generated_poc/core/push_notification_config.py +1 -4
  29. adcp/types/generated_poc/core/start_timing.py +18 -0
  30. adcp/types/generated_poc/core/sub_asset.py +12 -2
  31. adcp/types/generated_poc/creative/preview_creative_response.py +3 -14
  32. adcp/types/generated_poc/creative/preview_render.py +3 -11
  33. adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py +37 -0
  34. adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py +19 -0
  35. adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py +31 -0
  36. adcp/types/generated_poc/media_buy/create_media_buy_request.py +7 -26
  37. adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +4 -2
  38. adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py +38 -0
  39. adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py +24 -0
  40. adcp/types/generated_poc/media_buy/get_products_async_response_working.py +35 -0
  41. adcp/types/generated_poc/media_buy/get_products_request.py +5 -20
  42. adcp/types/generated_poc/media_buy/list_creatives_response.py +5 -7
  43. adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py +31 -0
  44. adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py +19 -0
  45. adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py +37 -0
  46. adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py +30 -0
  47. adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py +19 -0
  48. adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py +31 -0
  49. adcp/types/generated_poc/media_buy/update_media_buy_request.py +4 -14
  50. adcp/types/generated_poc/signals/activate_signal_request.py +2 -2
  51. adcp/types/generated_poc/signals/activate_signal_response.py +2 -2
  52. adcp/types/generated_poc/signals/get_signals_request.py +2 -2
  53. adcp/types/generated_poc/signals/get_signals_response.py +2 -3
  54. adcp/utils/preview_cache.py +6 -4
  55. adcp/webhooks.py +508 -0
  56. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/METADATA +2 -2
  57. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/RECORD +61 -45
  58. adcp/types/generated_poc/core/dimensions.py +0 -18
  59. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/WHEEL +0 -0
  60. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/entry_points.txt +0 -0
  61. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
  62. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,38 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/get_products_async_response_input_required.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Annotated
9
+
10
+ from adcp.types.base import AdCPBaseModel
11
+ from pydantic import ConfigDict, Field
12
+
13
+ from ..core import context as context_1
14
+ from ..core import ext as ext_1
15
+ from ..core import product
16
+
17
+
18
+ class Reason(Enum):
19
+ CLARIFICATION_NEEDED = 'CLARIFICATION_NEEDED'
20
+ BUDGET_REQUIRED = 'BUDGET_REQUIRED'
21
+
22
+
23
+ class GetProductsInputRequired(AdCPBaseModel):
24
+ model_config = ConfigDict(
25
+ extra='forbid',
26
+ )
27
+ context: context_1.ContextObject | None = None
28
+ ext: ext_1.ExtensionObject | None = None
29
+ partial_results: Annotated[
30
+ list[product.Product] | None,
31
+ Field(description='Partial product results that may help inform the clarification'),
32
+ ] = None
33
+ reason: Annotated[
34
+ Reason | None, Field(description='Reason code indicating why input is needed')
35
+ ] = None
36
+ suggestions: Annotated[
37
+ list[str] | None, Field(description='Suggested values or options for the required input')
38
+ ] = None
@@ -0,0 +1,24 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/get_products_async_response_submitted.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated
8
+
9
+ from adcp.types.base import AdCPBaseModel
10
+ from pydantic import AwareDatetime, ConfigDict, Field
11
+
12
+ from ..core import context as context_1
13
+ from ..core import ext as ext_1
14
+
15
+
16
+ class GetProductsSubmitted(AdCPBaseModel):
17
+ model_config = ConfigDict(
18
+ extra='forbid',
19
+ )
20
+ context: context_1.ContextObject | None = None
21
+ estimated_completion: Annotated[
22
+ AwareDatetime | None, Field(description='Estimated completion time for the search')
23
+ ] = None
24
+ ext: ext_1.ExtensionObject | None = None
@@ -0,0 +1,35 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/get_products_async_response_working.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated
8
+
9
+ from adcp.types.base import AdCPBaseModel
10
+ from pydantic import ConfigDict, Field
11
+
12
+ from ..core import context as context_1
13
+ from ..core import ext as ext_1
14
+
15
+
16
+ class GetProductsWorking(AdCPBaseModel):
17
+ model_config = ConfigDict(
18
+ extra='forbid',
19
+ )
20
+ context: context_1.ContextObject | None = None
21
+ current_step: Annotated[
22
+ str | None,
23
+ Field(
24
+ description="Current step in the search process (e.g., 'searching_inventory', 'validating_availability')"
25
+ ),
26
+ ] = None
27
+ ext: ext_1.ExtensionObject | None = None
28
+ percentage: Annotated[
29
+ float | None,
30
+ Field(description='Progress percentage of the search operation', ge=0.0, le=100.0),
31
+ ] = None
32
+ step_number: Annotated[int | None, Field(description='Current step number (1-indexed)')] = None
33
+ total_steps: Annotated[
34
+ int | None, Field(description='Total number of steps in the search process')
35
+ ] = None
@@ -1,15 +1,15 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: media_buy/get_products_request.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
7
  from typing import Annotated
8
8
 
9
9
  from adcp.types.base import AdCPBaseModel
10
- from pydantic import AnyUrl, ConfigDict, Field
10
+ from pydantic import ConfigDict, Field
11
11
 
12
- from ..core import brand_manifest as brand_manifest_1
12
+ from ..core import brand_manifest_ref
13
13
  from ..core import context as context_1
14
14
  from ..core import ext as ext_1
15
15
  from ..core import product_filters
@@ -20,24 +20,9 @@ class GetProductsRequest(AdCPBaseModel):
20
20
  extra='forbid',
21
21
  )
22
22
  brand_manifest: Annotated[
23
- brand_manifest_1.BrandManifest | AnyUrl | None,
23
+ brand_manifest_ref.BrandManifestReference | None,
24
24
  Field(
25
- description='Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.',
26
- examples=[
27
- {
28
- 'data': {
29
- 'colors': {'primary': '#FF6B35'},
30
- 'name': 'ACME Corporation',
31
- 'url': 'https://acmecorp.com',
32
- },
33
- 'description': 'Inline brand manifest',
34
- },
35
- {
36
- 'data': 'https://cdn.acmecorp.com/brand-manifest.json',
37
- 'description': 'URL string reference to hosted manifest',
38
- },
39
- ],
40
- title='Brand Manifest Reference',
25
+ description='Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.'
41
26
  ),
42
27
  ] = None
43
28
  brief: Annotated[
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: media_buy/list_creatives_response.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -147,12 +147,10 @@ class Creative(AdCPBaseModel):
147
147
  | html_asset.HtmlAsset
148
148
  | css_asset.CssAsset
149
149
  | javascript_asset.JavascriptAsset
150
+ | vast_asset.VastAsset
151
+ | daast_asset.DaastAsset
150
152
  | promoted_offerings.PromotedOfferings
151
- | url_asset.UrlAsset
152
- | vast_asset.VastAsset1
153
- | vast_asset.VastAsset2
154
- | daast_asset.DaastAsset1
155
- | daast_asset.DaastAsset2,
153
+ | url_asset.UrlAsset,
156
154
  ]
157
155
  | None,
158
156
  Field(description='Assets for this creative, keyed by asset_role'),
@@ -180,7 +178,7 @@ class Creative(AdCPBaseModel):
180
178
  creative_status.CreativeStatus, Field(description='Current approval status of the creative')
181
179
  ]
182
180
  sub_assets: Annotated[
183
- list[sub_asset.SubAsset1 | sub_asset.SubAsset2] | None,
181
+ list[sub_asset.SubAsset] | None,
184
182
  Field(
185
183
  description='Sub-assets for multi-asset formats (included when include_sub_assets=true)'
186
184
  ),
@@ -0,0 +1,31 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/sync_creatives_async_response_input_required.json
3
+ # timestamp: 2025-12-18T20:00:24+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Annotated
9
+
10
+ from adcp.types.base import AdCPBaseModel
11
+ from pydantic import ConfigDict, Field
12
+
13
+ from ..core import context as context_1
14
+ from ..core import ext as ext_1
15
+
16
+
17
+ class Reason(Enum):
18
+ APPROVAL_REQUIRED = 'APPROVAL_REQUIRED'
19
+ ASSET_CONFIRMATION = 'ASSET_CONFIRMATION'
20
+ FORMAT_CLARIFICATION = 'FORMAT_CLARIFICATION'
21
+
22
+
23
+ class SyncCreativesInputRequired(AdCPBaseModel):
24
+ model_config = ConfigDict(
25
+ extra='forbid',
26
+ )
27
+ context: context_1.ContextObject | None = None
28
+ ext: ext_1.ExtensionObject | None = None
29
+ reason: Annotated[
30
+ Reason | None, Field(description='Reason code indicating why buyer input is needed')
31
+ ] = None
@@ -0,0 +1,19 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/sync_creatives_async_response_submitted.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from adcp.types.base import AdCPBaseModel
8
+ from pydantic import ConfigDict
9
+
10
+ from ..core import context as context_1
11
+ from ..core import ext as ext_1
12
+
13
+
14
+ class SyncCreativesSubmitted(AdCPBaseModel):
15
+ model_config = ConfigDict(
16
+ extra='forbid',
17
+ )
18
+ context: context_1.ContextObject | None = None
19
+ ext: ext_1.ExtensionObject | None = None
@@ -0,0 +1,37 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/sync_creatives_async_response_working.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated
8
+
9
+ from adcp.types.base import AdCPBaseModel
10
+ from pydantic import ConfigDict, Field
11
+
12
+ from ..core import context as context_1
13
+ from ..core import ext as ext_1
14
+
15
+
16
+ class SyncCreativesWorking(AdCPBaseModel):
17
+ model_config = ConfigDict(
18
+ extra='forbid',
19
+ )
20
+ context: context_1.ContextObject | None = None
21
+ creatives_processed: Annotated[
22
+ int | None, Field(description='Number of creatives processed so far', ge=0)
23
+ ] = None
24
+ creatives_total: Annotated[
25
+ int | None, Field(description='Total number of creatives to process', ge=0)
26
+ ] = None
27
+ current_step: Annotated[
28
+ str | None, Field(description='Current step or phase of the operation')
29
+ ] = None
30
+ ext: ext_1.ExtensionObject | None = None
31
+ percentage: Annotated[
32
+ float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0)
33
+ ] = None
34
+ step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None
35
+ total_steps: Annotated[
36
+ int | None, Field(description='Total number of steps in the operation', ge=1)
37
+ ] = None
@@ -0,0 +1,30 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/update_media_buy_async_response_input_required.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Annotated
9
+
10
+ from adcp.types.base import AdCPBaseModel
11
+ from pydantic import ConfigDict, Field
12
+
13
+ from ..core import context as context_1
14
+ from ..core import ext as ext_1
15
+
16
+
17
+ class Reason(Enum):
18
+ APPROVAL_REQUIRED = 'APPROVAL_REQUIRED'
19
+ CHANGE_CONFIRMATION = 'CHANGE_CONFIRMATION'
20
+
21
+
22
+ class UpdateMediaBuyInputRequired(AdCPBaseModel):
23
+ model_config = ConfigDict(
24
+ extra='forbid',
25
+ )
26
+ context: context_1.ContextObject | None = None
27
+ ext: ext_1.ExtensionObject | None = None
28
+ reason: Annotated[
29
+ Reason | None, Field(description='Reason code indicating why input is needed')
30
+ ] = None
@@ -0,0 +1,19 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/update_media_buy_async_response_submitted.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from adcp.types.base import AdCPBaseModel
8
+ from pydantic import ConfigDict
9
+
10
+ from ..core import context as context_1
11
+ from ..core import ext as ext_1
12
+
13
+
14
+ class UpdateMediaBuySubmitted(AdCPBaseModel):
15
+ model_config = ConfigDict(
16
+ extra='forbid',
17
+ )
18
+ context: context_1.ContextObject | None = None
19
+ ext: ext_1.ExtensionObject | None = None
@@ -0,0 +1,31 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: media_buy/update_media_buy_async_response_working.json
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated
8
+
9
+ from adcp.types.base import AdCPBaseModel
10
+ from pydantic import ConfigDict, Field
11
+
12
+ from ..core import context as context_1
13
+ from ..core import ext as ext_1
14
+
15
+
16
+ class UpdateMediaBuyWorking(AdCPBaseModel):
17
+ model_config = ConfigDict(
18
+ extra='forbid',
19
+ )
20
+ context: context_1.ContextObject | None = None
21
+ current_step: Annotated[
22
+ str | None, Field(description='Current step or phase of the operation')
23
+ ] = None
24
+ ext: ext_1.ExtensionObject | None = None
25
+ percentage: Annotated[
26
+ float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0)
27
+ ] = None
28
+ step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None
29
+ total_steps: Annotated[
30
+ int | None, Field(description='Total number of steps in the operation', ge=1)
31
+ ] = None
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: media_buy/update_media_buy_request.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -13,7 +13,7 @@ from ..core import context as context_1
13
13
  from ..core import creative_asset, creative_assignment
14
14
  from ..core import ext as ext_1
15
15
  from ..core import push_notification_config as push_notification_config_1
16
- from ..core import targeting
16
+ from ..core import start_timing, targeting
17
17
  from ..enums import pacing as pacing_1
18
18
 
19
19
 
@@ -137,12 +137,7 @@ class UpdateMediaBuyRequest1(AdCPBaseModel):
137
137
  description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.'
138
138
  ),
139
139
  ] = None
140
- start_time: Annotated[
141
- str | AwareDatetime | None,
142
- Field(
143
- description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing'
144
- ),
145
- ] = None
140
+ start_time: start_timing.StartTiming | None = None
146
141
 
147
142
 
148
143
  Packages2 = Packages
@@ -177,12 +172,7 @@ class UpdateMediaBuyRequest2(AdCPBaseModel):
177
172
  description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.'
178
173
  ),
179
174
  ] = None
180
- start_time: Annotated[
181
- str | AwareDatetime | None,
182
- Field(
183
- description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing'
184
- ),
185
- ] = None
175
+ start_time: start_timing.StartTiming | None = None
186
176
 
187
177
 
188
178
  class UpdateMediaBuyRequest(RootModel[UpdateMediaBuyRequest1 | UpdateMediaBuyRequest2]):
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/activate_signal_request.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -20,7 +20,7 @@ class ActivateSignalRequest(AdCPBaseModel):
20
20
  )
21
21
  context: context_1.ContextObject | None = None
22
22
  deployments: Annotated[
23
- list[destination.Destination1 | destination.Destination2],
23
+ list[destination.Destination],
24
24
  Field(
25
25
  description='Target deployment(s) for activation. If the authenticated caller matches one of these deployment targets, activation keys will be included in the response.',
26
26
  min_length=1,
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/activate_signal_response.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -20,7 +20,7 @@ class ActivateSignalResponse1(AdCPBaseModel):
20
20
  )
21
21
  context: context_1.ContextObject | None = None
22
22
  deployments: Annotated[
23
- list[deployment.Deployment1 | deployment.Deployment2],
23
+ list[deployment.Deployment],
24
24
  Field(description='Array of deployment results for each deployment target'),
25
25
  ]
26
26
  ext: ext_1.ExtensionObject | None = None
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/get_signals_request.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -27,7 +27,7 @@ class DeliverTo(AdCPBaseModel):
27
27
  list[Country], Field(description='Countries where signals will be used (ISO codes)')
28
28
  ]
29
29
  deployments: Annotated[
30
- list[destination.Destination1 | destination.Destination2],
30
+ list[destination.Destination],
31
31
  Field(
32
32
  description='List of deployment targets (DSPs, sales agents, etc.). If the authenticated caller matches one of these deployment targets, activation keys will be included in the response.',
33
33
  min_length=1,
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: signals/get_signals_response.json
3
- # timestamp: 2025-11-29T12:00:45+00:00
3
+ # timestamp: 2025-12-11T15:09:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -32,8 +32,7 @@ class Signal(AdCPBaseModel):
32
32
  ]
33
33
  data_provider: Annotated[str, Field(description='Name of the data provider')]
34
34
  deployments: Annotated[
35
- list[deployment.Deployment1 | deployment.Deployment2],
36
- Field(description='Array of deployment targets'),
35
+ list[deployment.Deployment], Field(description='Array of deployment targets')
37
36
  ]
38
37
  description: Annotated[str, Field(description='Detailed signal description')]
39
38
  name: Annotated[str, Field(description='Human-readable signal name')]
@@ -87,13 +87,15 @@ class PreviewURLGenerator:
87
87
  first_render = preview.renders[0] if preview.renders else None
88
88
 
89
89
  if first_render:
90
- has_url = hasattr(first_render, "preview_url")
91
- preview_url = str(first_render.preview_url) if has_url else None
90
+ # PreviewRender is a RootModel, access attributes via .root
91
+ render = first_render.root
92
+ has_url = hasattr(render, "preview_url")
93
+ preview_url = str(render.preview_url) if has_url else None
92
94
  preview_data = {
93
95
  "preview_id": preview.preview_id,
94
96
  "preview_url": preview_url,
95
- "preview_html": getattr(first_render, "preview_html", None),
96
- "render_id": first_render.render_id,
97
+ "preview_html": getattr(render, "preview_html", None),
98
+ "render_id": render.render_id,
97
99
  "input": preview.input.model_dump(),
98
100
  "expires_at": str(result.data.expires_at),
99
101
  }