adcp 2.18.0__py3-none-any.whl → 3.0.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 +6 -14
- adcp/__main__.py +94 -51
- adcp/adagents.py +91 -19
- adcp/client.py +865 -0
- adcp/protocols/a2a.py +84 -0
- adcp/protocols/base.py +101 -0
- adcp/protocols/mcp.py +87 -1
- adcp/server/__init__.py +49 -0
- adcp/server/base.py +368 -0
- adcp/server/content_standards.py +561 -0
- adcp/server/governance.py +491 -0
- adcp/server/mcp_tools.py +471 -0
- adcp/server/proposal.py +334 -0
- adcp/server/sponsored_intelligence.py +444 -0
- adcp/types/__init__.py +111 -23
- adcp/types/_ergonomic.py +35 -18
- adcp/types/_generated.py +419 -44
- adcp/types/aliases.py +13 -20
- adcp/types/base.py +1 -1
- adcp/types/generated_poc/adagents.py +103 -6
- adcp/types/generated_poc/content_standards/__init__.py +3 -0
- adcp/types/generated_poc/content_standards/artifact.py +208 -0
- adcp/types/generated_poc/content_standards/artifact_webhook_payload.py +64 -0
- adcp/types/generated_poc/content_standards/calibrate_content_request.py +17 -0
- adcp/types/generated_poc/content_standards/calibrate_content_response.py +74 -0
- adcp/types/generated_poc/content_standards/content_standards.py +66 -0
- adcp/types/generated_poc/content_standards/create_content_standards_request.py +97 -0
- adcp/types/generated_poc/content_standards/create_content_standards_response.py +52 -0
- adcp/types/generated_poc/content_standards/get_content_standards_request.py +21 -0
- adcp/types/generated_poc/content_standards/get_content_standards_response.py +43 -0
- adcp/types/generated_poc/content_standards/get_media_buy_artifacts_request.py +64 -0
- adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py +117 -0
- adcp/types/generated_poc/content_standards/list_content_standards_request.py +31 -0
- adcp/types/generated_poc/content_standards/list_content_standards_response.py +48 -0
- adcp/types/generated_poc/content_standards/update_content_standards_request.py +101 -0
- adcp/types/generated_poc/content_standards/update_content_standards_response.py +34 -0
- adcp/types/generated_poc/content_standards/validate_content_delivery_request.py +59 -0
- adcp/types/generated_poc/content_standards/validate_content_delivery_response.py +85 -0
- adcp/types/generated_poc/core/activation_key.py +1 -1
- adcp/types/generated_poc/core/assets/audio_asset.py +1 -1
- adcp/types/generated_poc/core/assets/css_asset.py +1 -1
- adcp/types/generated_poc/core/assets/daast_asset.py +1 -1
- adcp/types/generated_poc/core/assets/html_asset.py +1 -1
- adcp/types/generated_poc/core/assets/image_asset.py +1 -1
- adcp/types/generated_poc/core/assets/javascript_asset.py +1 -1
- adcp/types/generated_poc/core/assets/text_asset.py +1 -1
- adcp/types/generated_poc/core/assets/url_asset.py +1 -1
- adcp/types/generated_poc/core/assets/vast_asset.py +1 -1
- adcp/types/generated_poc/core/assets/video_asset.py +1 -1
- adcp/types/generated_poc/core/assets/webhook_asset.py +1 -1
- adcp/types/generated_poc/core/async_response_data.py +1 -1
- adcp/types/generated_poc/core/brand_manifest.py +68 -5
- adcp/types/generated_poc/core/brand_manifest_ref.py +1 -1
- adcp/types/generated_poc/core/context.py +1 -1
- adcp/types/generated_poc/core/creative_asset.py +8 -7
- adcp/types/generated_poc/core/creative_assignment.py +1 -1
- adcp/types/generated_poc/core/creative_filters.py +4 -14
- adcp/types/generated_poc/core/creative_manifest.py +1 -1
- adcp/types/generated_poc/core/creative_policy.py +1 -1
- adcp/types/generated_poc/core/delivery_metrics.py +1 -1
- adcp/types/generated_poc/core/deployment.py +1 -1
- adcp/types/generated_poc/core/destination.py +1 -1
- adcp/types/generated_poc/core/error.py +1 -1
- adcp/types/generated_poc/core/ext.py +1 -1
- adcp/types/generated_poc/core/format.py +6 -5
- adcp/types/generated_poc/core/format_id.py +1 -1
- adcp/types/generated_poc/core/frequency_cap.py +1 -1
- adcp/types/generated_poc/core/identifier.py +27 -0
- adcp/types/generated_poc/core/mcp_webhook_payload.py +1 -1
- adcp/types/generated_poc/core/measurement.py +1 -1
- adcp/types/generated_poc/core/media_buy.py +1 -1
- adcp/types/generated_poc/core/media_buy_features.py +29 -0
- adcp/types/generated_poc/core/offering.py +80 -0
- adcp/types/generated_poc/core/package.py +1 -1
- adcp/types/generated_poc/core/performance_feedback.py +1 -1
- adcp/types/generated_poc/core/placement.py +1 -1
- adcp/types/generated_poc/core/pricing_option.py +8 -14
- adcp/types/generated_poc/core/product.py +1 -1
- adcp/types/generated_poc/core/product_allocation.py +48 -0
- adcp/types/generated_poc/core/product_filters.py +72 -7
- adcp/types/generated_poc/core/promoted_offerings.py +12 -21
- adcp/types/generated_poc/core/promoted_products.py +1 -1
- adcp/types/generated_poc/core/property.py +1 -1
- adcp/types/generated_poc/core/property_id.py +1 -1
- adcp/types/generated_poc/core/property_list_ref.py +26 -0
- adcp/types/generated_poc/core/property_tag.py +1 -1
- adcp/types/generated_poc/core/proposal.py +64 -0
- adcp/types/generated_poc/core/protocol_envelope.py +1 -1
- adcp/types/generated_poc/core/publisher_property_selector.py +1 -1
- adcp/types/generated_poc/core/push_notification_config.py +1 -1
- adcp/types/generated_poc/core/reporting_capabilities.py +1 -1
- adcp/types/generated_poc/core/reporting_webhook.py +70 -0
- adcp/types/generated_poc/core/response.py +1 -1
- adcp/types/generated_poc/core/signal_filters.py +1 -1
- adcp/types/generated_poc/core/start_timing.py +4 -4
- adcp/types/generated_poc/core/sub_asset.py +1 -1
- adcp/types/generated_poc/core/targeting.py +55 -14
- adcp/types/generated_poc/creative/list_creative_formats_request.py +1 -1
- adcp/types/generated_poc/creative/list_creative_formats_response.py +1 -1
- adcp/types/generated_poc/creative/preview_creative_request.py +1 -1
- adcp/types/generated_poc/creative/preview_creative_response.py +3 -2
- adcp/types/generated_poc/creative/preview_render.py +1 -1
- adcp/types/generated_poc/enums/adcp_domain.py +3 -1
- adcp/types/generated_poc/enums/asset_content_type.py +1 -1
- adcp/types/generated_poc/enums/auth_scheme.py +1 -1
- adcp/types/generated_poc/enums/available_metric.py +1 -1
- adcp/types/generated_poc/enums/channels.py +18 -8
- adcp/types/generated_poc/enums/co_branding_requirement.py +1 -1
- adcp/types/generated_poc/enums/creative_action.py +1 -1
- adcp/types/generated_poc/enums/creative_agent_capability.py +1 -1
- adcp/types/generated_poc/enums/creative_sort_field.py +1 -1
- adcp/types/generated_poc/enums/creative_status.py +2 -1
- adcp/types/generated_poc/enums/daast_tracking_event.py +1 -1
- adcp/types/generated_poc/enums/daast_version.py +1 -1
- adcp/types/generated_poc/enums/delivery_type.py +1 -1
- adcp/types/generated_poc/enums/dimension_unit.py +1 -1
- adcp/types/generated_poc/enums/feed_format.py +1 -1
- adcp/types/generated_poc/enums/feedback_source.py +1 -1
- adcp/types/generated_poc/enums/format_category.py +1 -1
- adcp/types/generated_poc/enums/format_id_parameter.py +1 -1
- adcp/types/generated_poc/enums/frequency_cap_scope.py +1 -1
- adcp/types/generated_poc/enums/geo_level.py +14 -0
- adcp/types/generated_poc/enums/history_entry_type.py +1 -1
- adcp/types/generated_poc/enums/http_method.py +1 -1
- adcp/types/generated_poc/enums/identifier_types.py +1 -1
- adcp/types/generated_poc/enums/javascript_module_type.py +1 -1
- adcp/types/generated_poc/enums/landing_page_requirement.py +1 -1
- adcp/types/generated_poc/enums/markdown_flavor.py +1 -1
- adcp/types/generated_poc/enums/media_buy_status.py +1 -1
- adcp/types/generated_poc/enums/metric_type.py +1 -1
- adcp/types/generated_poc/enums/metro_system.py +15 -0
- adcp/types/generated_poc/enums/notification_type.py +1 -1
- adcp/types/generated_poc/enums/pacing.py +1 -1
- adcp/types/generated_poc/enums/postal_system.py +19 -0
- adcp/types/generated_poc/enums/preview_output_format.py +1 -1
- adcp/types/generated_poc/enums/pricing_model.py +1 -1
- adcp/types/generated_poc/enums/property_type.py +2 -1
- adcp/types/generated_poc/enums/publisher_identifier_types.py +1 -1
- adcp/types/generated_poc/enums/reporting_frequency.py +1 -1
- adcp/types/generated_poc/enums/signal_catalog_type.py +1 -1
- adcp/types/generated_poc/enums/sort_direction.py +1 -1
- adcp/types/generated_poc/enums/task_status.py +1 -1
- adcp/types/generated_poc/enums/task_type.py +7 -1
- adcp/types/generated_poc/enums/update_frequency.py +1 -1
- adcp/types/generated_poc/enums/url_asset_type.py +1 -1
- adcp/types/generated_poc/enums/validation_mode.py +1 -1
- adcp/types/generated_poc/enums/vast_tracking_event.py +1 -1
- adcp/types/generated_poc/enums/vast_version.py +1 -1
- adcp/types/generated_poc/enums/webhook_response_type.py +1 -1
- adcp/types/generated_poc/enums/webhook_security_method.py +1 -1
- adcp/types/generated_poc/extensions/__init__.py +3 -0
- adcp/types/generated_poc/extensions/extension_meta.py +58 -0
- adcp/types/generated_poc/media_buy/build_creative_request.py +1 -1
- adcp/types/generated_poc/media_buy/build_creative_response.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_request.py +54 -26
- adcp/types/generated_poc/media_buy/create_media_buy_response.py +1 -1
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +1 -1
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_async_response_working.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_request.py +18 -3
- adcp/types/generated_poc/media_buy/get_products_response.py +14 -2
- adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +1 -1
- adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +2 -2
- adcp/types/generated_poc/media_buy/list_creative_formats_request.py +1 -1
- adcp/types/generated_poc/media_buy/list_creative_formats_response.py +1 -1
- adcp/types/generated_poc/media_buy/list_creatives_request.py +2 -2
- adcp/types/generated_poc/media_buy/list_creatives_response.py +7 -10
- adcp/types/generated_poc/media_buy/package_request.py +15 -6
- adcp/types/generated_poc/media_buy/package_update.py +119 -0
- adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +1 -1
- adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_request.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_response.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_request.py +20 -108
- adcp/types/generated_poc/media_buy/update_media_buy_response.py +1 -1
- adcp/types/generated_poc/pricing_options/cpc_option.py +35 -10
- adcp/types/generated_poc/pricing_options/cpcv_option.py +35 -10
- adcp/types/generated_poc/pricing_options/{cpm_auction_option.py → cpm_option.py} +23 -19
- adcp/types/generated_poc/pricing_options/cpp_option.py +39 -16
- adcp/types/generated_poc/pricing_options/cpv_option.py +37 -18
- adcp/types/generated_poc/pricing_options/flat_rate_option.py +45 -39
- adcp/types/generated_poc/pricing_options/{vcpm_auction_option.py → vcpm_option.py} +23 -14
- adcp/types/generated_poc/property/__init__.py +3 -0
- adcp/types/generated_poc/property/base_property_source.py +86 -0
- adcp/types/generated_poc/property/create_property_list_request.py +43 -0
- adcp/types/generated_poc/property/create_property_list_response.py +27 -0
- adcp/types/generated_poc/property/delete_property_list_request.py +22 -0
- adcp/types/generated_poc/property/delete_property_list_response.py +21 -0
- adcp/types/generated_poc/property/feature_requirement.py +42 -0
- adcp/types/generated_poc/property/get_property_list_request.py +34 -0
- adcp/types/generated_poc/property/get_property_list_response.py +61 -0
- adcp/types/generated_poc/property/list_property_lists_request.py +29 -0
- adcp/types/generated_poc/property/list_property_lists_response.py +39 -0
- adcp/types/generated_poc/property/property_error.py +33 -0
- adcp/types/generated_poc/property/property_feature.py +22 -0
- adcp/types/generated_poc/property/property_feature_definition.py +80 -0
- adcp/types/generated_poc/property/property_list.py +62 -0
- adcp/types/generated_poc/property/property_list_changed_webhook.py +51 -0
- adcp/types/generated_poc/property/property_list_filters.py +47 -0
- adcp/types/generated_poc/property/update_property_list_request.py +46 -0
- adcp/types/generated_poc/property/update_property_list_response.py +21 -0
- adcp/types/generated_poc/protocol/__init__.py +3 -0
- adcp/types/generated_poc/protocol/get_adcp_capabilities_request.py +34 -0
- adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py +353 -0
- adcp/types/generated_poc/protocols/adcp_extension.py +18 -5
- adcp/types/generated_poc/signals/activate_signal_request.py +1 -1
- adcp/types/generated_poc/signals/activate_signal_response.py +1 -1
- adcp/types/generated_poc/signals/get_signals_request.py +1 -1
- adcp/types/generated_poc/signals/get_signals_response.py +1 -1
- adcp/types/generated_poc/sponsored_intelligence/__init__.py +3 -0
- adcp/types/generated_poc/sponsored_intelligence/si_capabilities.py +102 -0
- adcp/types/generated_poc/sponsored_intelligence/si_get_offering_request.py +34 -0
- adcp/types/generated_poc/sponsored_intelligence/si_get_offering_response.py +100 -0
- adcp/types/generated_poc/sponsored_intelligence/si_identity.py +78 -0
- adcp/types/generated_poc/sponsored_intelligence/si_initiate_session_request.py +46 -0
- adcp/types/generated_poc/sponsored_intelligence/si_initiate_session_response.py +44 -0
- adcp/types/generated_poc/sponsored_intelligence/si_send_message_request.py +58 -0
- adcp/types/generated_poc/sponsored_intelligence/si_send_message_response.py +101 -0
- adcp/types/generated_poc/sponsored_intelligence/si_terminate_session_request.py +60 -0
- adcp/types/generated_poc/sponsored_intelligence/si_terminate_session_response.py +54 -0
- adcp/types/generated_poc/sponsored_intelligence/si_ui_element.py +30 -0
- adcp/utils/format_assets.py +5 -5
- adcp/utils/preview_cache.py +2 -2
- {adcp-2.18.0.dist-info → adcp-3.0.0.dist-info}/METADATA +1 -1
- adcp-3.0.0.dist-info/RECORD +264 -0
- {adcp-2.18.0.dist-info → adcp-3.0.0.dist-info}/WHEEL +1 -1
- adcp/types/generated_poc/enums/standard_format_ids.py +0 -45
- adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +0 -43
- adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +0 -47
- adcp-2.18.0.dist-info/RECORD +0 -195
- {adcp-2.18.0.dist-info → adcp-3.0.0.dist-info}/entry_points.txt +0 -0
- {adcp-2.18.0.dist-info → adcp-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {adcp-2.18.0.dist-info → adcp-3.0.0.dist-info}/top_level.txt +0 -0
adcp/server/proposal.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Proposal generation helpers.
|
|
2
|
+
|
|
3
|
+
Provides utilities for building ADCP Proposals in get_products responses.
|
|
4
|
+
Proposals represent recommended media plans with budget allocations across products.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Any
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from adcp.types import Error
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProposalNotSupported(BaseModel):
|
|
19
|
+
"""Response indicating proposal generation is not supported.
|
|
20
|
+
|
|
21
|
+
Use this when your agent supports get_products but not proposal generation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
proposals_supported: bool = False
|
|
25
|
+
reason: str = "This agent does not generate proposals"
|
|
26
|
+
error: Error | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def proposals_not_supported(
|
|
30
|
+
reason: str = "This agent does not generate proposals",
|
|
31
|
+
) -> ProposalNotSupported:
|
|
32
|
+
"""Create a response indicating proposals are not supported.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
reason: Human-readable explanation
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ProposalNotSupported response
|
|
39
|
+
"""
|
|
40
|
+
return ProposalNotSupported(
|
|
41
|
+
proposals_supported=False,
|
|
42
|
+
reason=reason,
|
|
43
|
+
error=Error(
|
|
44
|
+
code="PROPOSALS_NOT_SUPPORTED",
|
|
45
|
+
message=reason,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AllocationBuilder:
|
|
51
|
+
"""Builder for product allocations within a proposal."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
product_id: str,
|
|
56
|
+
allocation_percentage: float,
|
|
57
|
+
):
|
|
58
|
+
"""Create an allocation builder.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
product_id: ID of the product (must match a product in the response)
|
|
62
|
+
allocation_percentage: Percentage of budget (0-100)
|
|
63
|
+
"""
|
|
64
|
+
self._data: dict[str, Any] = {
|
|
65
|
+
"product_id": product_id,
|
|
66
|
+
"allocation_percentage": allocation_percentage,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def with_pricing_option(self, pricing_option_id: str) -> AllocationBuilder:
|
|
70
|
+
"""Specify which pricing option to use.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
pricing_option_id: ID from the product's pricing_options array
|
|
74
|
+
"""
|
|
75
|
+
self._data["pricing_option_id"] = pricing_option_id
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def with_rationale(self, rationale: str) -> AllocationBuilder:
|
|
79
|
+
"""Add explanation for this allocation.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
rationale: Why this product/allocation is recommended
|
|
83
|
+
"""
|
|
84
|
+
self._data["rationale"] = rationale
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def with_sequence(self, sequence: int) -> AllocationBuilder:
|
|
88
|
+
"""Set ordering hint for multi-line-item plans.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
sequence: 1-based ordering position
|
|
92
|
+
"""
|
|
93
|
+
self._data["sequence"] = sequence
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def with_tags(self, tags: list[str]) -> AllocationBuilder:
|
|
97
|
+
"""Add categorical tags.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tags: Tags like 'desktop', 'mobile', 'german'
|
|
101
|
+
"""
|
|
102
|
+
self._data["tags"] = tags
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def build(self) -> dict[str, Any]:
|
|
106
|
+
"""Build the allocation dict."""
|
|
107
|
+
return self._data.copy()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ProposalBuilder:
|
|
111
|
+
"""Builder for ADCP Proposals.
|
|
112
|
+
|
|
113
|
+
Helps construct valid proposals for get_products responses. Proposals
|
|
114
|
+
represent recommended media plans with budget allocations.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
proposal = (
|
|
118
|
+
ProposalBuilder("Q1 Brand Campaign")
|
|
119
|
+
.with_description("Balanced awareness campaign")
|
|
120
|
+
.add_allocation("product-1", 60)
|
|
121
|
+
.with_rationale("High-impact display")
|
|
122
|
+
.add_allocation("product-2", 40)
|
|
123
|
+
.with_rationale("Contextual targeting")
|
|
124
|
+
.with_budget_guidance(min=10000, recommended=25000, max=50000)
|
|
125
|
+
.build()
|
|
126
|
+
)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, name: str, proposal_id: str | None = None):
|
|
130
|
+
"""Create a new proposal builder.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Human-readable name for the proposal
|
|
134
|
+
proposal_id: Unique ID (auto-generated if not provided)
|
|
135
|
+
"""
|
|
136
|
+
self._name = name
|
|
137
|
+
self._proposal_id = proposal_id or f"proposal-{uuid4().hex[:8]}"
|
|
138
|
+
self._description: str | None = None
|
|
139
|
+
self._brief_alignment: str | None = None
|
|
140
|
+
self._expires_at: datetime | None = None
|
|
141
|
+
self._allocations: list[dict[str, Any]] = []
|
|
142
|
+
self._budget_guidance: dict[str, Any] | None = None
|
|
143
|
+
self._current_allocation: AllocationBuilder | None = None
|
|
144
|
+
self._ext: dict[str, Any] | None = None
|
|
145
|
+
|
|
146
|
+
def with_description(self, description: str) -> ProposalBuilder:
|
|
147
|
+
"""Add description explaining the proposal strategy.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
description: What the proposal achieves
|
|
151
|
+
"""
|
|
152
|
+
self._finalize_allocation()
|
|
153
|
+
self._description = description
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def with_brief_alignment(self, alignment: str) -> ProposalBuilder:
|
|
157
|
+
"""Explain how proposal aligns with campaign brief.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
alignment: Alignment explanation
|
|
161
|
+
"""
|
|
162
|
+
self._finalize_allocation()
|
|
163
|
+
self._brief_alignment = alignment
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def expires_in(self, days: int = 7) -> ProposalBuilder:
|
|
167
|
+
"""Set expiration relative to now.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
days: Number of days until expiration
|
|
171
|
+
"""
|
|
172
|
+
self._finalize_allocation()
|
|
173
|
+
self._expires_at = datetime.now(timezone.utc) + timedelta(days=days)
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def expires_at(self, expires: datetime) -> ProposalBuilder:
|
|
177
|
+
"""Set absolute expiration time.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
expires: When the proposal expires
|
|
181
|
+
"""
|
|
182
|
+
self._finalize_allocation()
|
|
183
|
+
self._expires_at = expires
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def add_allocation(
|
|
187
|
+
self,
|
|
188
|
+
product_id: str,
|
|
189
|
+
allocation_percentage: float,
|
|
190
|
+
) -> ProposalBuilder:
|
|
191
|
+
"""Add a product allocation.
|
|
192
|
+
|
|
193
|
+
After calling this, chain allocation methods (with_rationale, etc.)
|
|
194
|
+
before adding another allocation or calling build().
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
product_id: ID of the product
|
|
198
|
+
allocation_percentage: Percentage of budget (0-100)
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Self for method chaining
|
|
202
|
+
"""
|
|
203
|
+
self._finalize_allocation()
|
|
204
|
+
self._current_allocation = AllocationBuilder(product_id, allocation_percentage)
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def with_pricing_option(self, pricing_option_id: str) -> ProposalBuilder:
|
|
208
|
+
"""Set pricing option for current allocation."""
|
|
209
|
+
if self._current_allocation:
|
|
210
|
+
self._current_allocation.with_pricing_option(pricing_option_id)
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def with_rationale(self, rationale: str) -> ProposalBuilder:
|
|
214
|
+
"""Add rationale for current allocation."""
|
|
215
|
+
if self._current_allocation:
|
|
216
|
+
self._current_allocation.with_rationale(rationale)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def with_sequence(self, sequence: int) -> ProposalBuilder:
|
|
220
|
+
"""Set sequence for current allocation."""
|
|
221
|
+
if self._current_allocation:
|
|
222
|
+
self._current_allocation.with_sequence(sequence)
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def with_tags(self, tags: list[str]) -> ProposalBuilder:
|
|
226
|
+
"""Add tags for current allocation."""
|
|
227
|
+
if self._current_allocation:
|
|
228
|
+
self._current_allocation.with_tags(tags)
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def with_budget_guidance(
|
|
232
|
+
self,
|
|
233
|
+
*,
|
|
234
|
+
min: float | None = None,
|
|
235
|
+
recommended: float | None = None,
|
|
236
|
+
max: float | None = None,
|
|
237
|
+
currency: str = "USD",
|
|
238
|
+
) -> ProposalBuilder:
|
|
239
|
+
"""Add budget guidance for the proposal.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
min: Minimum recommended budget
|
|
243
|
+
recommended: Optimal budget
|
|
244
|
+
max: Maximum before diminishing returns
|
|
245
|
+
currency: ISO 4217 currency code
|
|
246
|
+
"""
|
|
247
|
+
self._finalize_allocation()
|
|
248
|
+
self._budget_guidance = {
|
|
249
|
+
"currency": currency,
|
|
250
|
+
}
|
|
251
|
+
if min is not None:
|
|
252
|
+
self._budget_guidance["min"] = min
|
|
253
|
+
if recommended is not None:
|
|
254
|
+
self._budget_guidance["recommended"] = recommended
|
|
255
|
+
if max is not None:
|
|
256
|
+
self._budget_guidance["max"] = max
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
def with_extension(self, ext: dict[str, Any]) -> ProposalBuilder:
|
|
260
|
+
"""Add extension data.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
ext: Extension object
|
|
264
|
+
"""
|
|
265
|
+
self._finalize_allocation()
|
|
266
|
+
self._ext = ext
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
def _finalize_allocation(self) -> None:
|
|
270
|
+
"""Finalize current allocation and add to list."""
|
|
271
|
+
if self._current_allocation:
|
|
272
|
+
self._allocations.append(self._current_allocation.build())
|
|
273
|
+
self._current_allocation = None
|
|
274
|
+
|
|
275
|
+
def build(self) -> dict[str, Any]:
|
|
276
|
+
"""Build the proposal dict.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Proposal as a dict ready for use in get_products response
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ValueError: If allocations don't sum to 100
|
|
283
|
+
"""
|
|
284
|
+
self._finalize_allocation()
|
|
285
|
+
|
|
286
|
+
if not self._allocations:
|
|
287
|
+
raise ValueError("Proposal must have at least one allocation")
|
|
288
|
+
|
|
289
|
+
total = sum(a["allocation_percentage"] for a in self._allocations)
|
|
290
|
+
if abs(total - 100.0) > 0.01:
|
|
291
|
+
raise ValueError(
|
|
292
|
+
f"Allocation percentages must sum to 100, got {total}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
proposal: dict[str, Any] = {
|
|
296
|
+
"proposal_id": self._proposal_id,
|
|
297
|
+
"name": self._name,
|
|
298
|
+
"allocations": self._allocations,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if self._description:
|
|
302
|
+
proposal["description"] = self._description
|
|
303
|
+
if self._brief_alignment:
|
|
304
|
+
proposal["brief_alignment"] = self._brief_alignment
|
|
305
|
+
if self._expires_at:
|
|
306
|
+
proposal["expires_at"] = self._expires_at.isoformat()
|
|
307
|
+
if self._budget_guidance:
|
|
308
|
+
proposal["total_budget_guidance"] = self._budget_guidance
|
|
309
|
+
if self._ext:
|
|
310
|
+
proposal["ext"] = self._ext
|
|
311
|
+
|
|
312
|
+
return proposal
|
|
313
|
+
|
|
314
|
+
def validate(self) -> list[str]:
|
|
315
|
+
"""Validate the proposal without building.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of validation errors (empty if valid)
|
|
319
|
+
"""
|
|
320
|
+
errors: list[str] = []
|
|
321
|
+
|
|
322
|
+
if self._current_allocation:
|
|
323
|
+
allocations = self._allocations + [self._current_allocation.build()]
|
|
324
|
+
else:
|
|
325
|
+
allocations = self._allocations
|
|
326
|
+
|
|
327
|
+
if not allocations:
|
|
328
|
+
errors.append("Proposal must have at least one allocation")
|
|
329
|
+
else:
|
|
330
|
+
total = sum(a["allocation_percentage"] for a in allocations)
|
|
331
|
+
if abs(total - 100.0) > 0.01:
|
|
332
|
+
errors.append(f"Allocation percentages must sum to 100, got {total}")
|
|
333
|
+
|
|
334
|
+
return errors
|