diff --git a/examples/seller_agent.py b/examples/seller_agent.py index 45601196..0c0a8ca9 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -107,6 +107,7 @@ async def get_adcp_capabilities( ) -> dict[str, Any]: return capabilities_response( ["media_buy"], + idempotency={"supported": False}, compliance_testing={ "scenarios": [ "force_account_status", @@ -223,14 +224,25 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> } ) + has_creatives = any( + pkg.get("creative_assignments") or pkg.get("creatives") + for pkg in params["packages"] + ) + status = "active" if has_creatives else "pending_creatives" + mb_id = f"mb-{uuid.uuid4().hex[:8]}" media_buys[mb_id] = { - "status": "active", + "status": status, "currency": "USD", "packages": packages, "revision": 1, } - return media_buy_response(mb_id, packages, status="active") + pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates", + "update_packages", "add_packages"] + return media_buy_response( + mb_id, packages, status=status, + valid_actions=pending_actions if status == "pending_creatives" else None, + ) async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]: requested_ids = params.get("media_buy_ids") @@ -238,12 +250,14 @@ async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> d for mb_id, mb in media_buys.items(): if requested_ids and mb_id not in requested_ids: continue + total_budget = sum((pkg.get("budget") or 0) for pkg in mb.get("packages", [])) results.append( { "media_buy_id": mb_id, "status": mb["status"], "currency": mb.get("currency", "USD"), "packages": mb.get("packages", []), + "total_budget": total_budget, } ) return media_buys_response(results) @@ -257,7 +271,25 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> if params.get("revision") and params["revision"] != mb.get("revision", 1): return adcp_error("CONFLICT", "Revision mismatch - refetch and retry") + if params.get("packages"): + existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])} + for pkg_update in params["packages"]: + pkg_id = pkg_update.get("package_id") + if pkg_id and pkg_id not in existing_pkg_ids: + return adcp_error( + "PACKAGE_NOT_FOUND", + f"Package '{pkg_id}' not found in media buy {mb_id}", + field="package_id", + ) + status = mb["status"] + if status == "pending_creatives" and params.get("packages"): + if any( + pkg.get("creative_assignments") or pkg.get("creatives") + for pkg in params["packages"] + ): + mb["status"] = "active" + status = "active" if params.get("paused") is True and status == "active": mb["status"] = "paused" elif params.get("paused") is False and status == "paused": @@ -269,55 +301,71 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> return cancel_media_buy_response(mb_id, "buyer") mb["revision"] = mb.get("revision", 1) + 1 - return update_media_buy_response(mb_id, status=mb["status"], revision=mb["revision"]) + pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates", + "update_packages", "add_packages"] + return update_media_buy_response( + mb_id, + status=mb["status"], + revision=mb["revision"], + valid_actions=pending_actions if mb["status"] == "pending_creatives" else None, + ) async def list_creative_formats( self, params: dict[str, Any], context: Any = None ) -> dict[str, Any]: - return creative_formats_response( - [ - { - "format_id": { - "agent_url": AGENT_URL, - "id": "display_300x250", - }, - "name": "Display 300x250", - "renders": [{"width": 300, "height": 250}], - "assets": [ - { - "item_type": "individual", - "asset_id": "image", - "asset_type": "image", - "required": True, - "accepted_media_types": [ - "image/png", - "image/jpeg", - ], - } - ], + all_formats: list[dict[str, Any]] = [ + { + "format_id": { + "agent_url": AGENT_URL, + "id": "display_300x250", }, - { - "format_id": { - "agent_url": AGENT_URL, - "id": "display_970x250", - }, - "name": "Display 970x250", - "renders": [{"width": 970, "height": 250}], - "assets": [ - { - "item_type": "individual", - "asset_id": "image", - "asset_type": "image", - "required": True, - "accepted_media_types": [ - "image/png", - "image/jpeg", - ], - } - ], + "name": "Display 300x250", + "renders": [{"role": "primary", "dimensions": {"width": 300, "height": 250}}], + "assets": [ + { + "item_type": "individual", + "asset_id": "image", + "asset_type": "image", + "required": True, + "accepted_media_types": [ + "image/png", + "image/jpeg", + ], + } + ], + }, + { + "format_id": { + "agent_url": AGENT_URL, + "id": "display_970x250", }, + "name": "Display 970x250", + "renders": [{"role": "primary", "dimensions": {"width": 970, "height": 250}}], + "assets": [ + { + "item_type": "individual", + "asset_id": "image", + "asset_type": "image", + "required": True, + "accepted_media_types": [ + "image/png", + "image/jpeg", + ], + } + ], + }, + ] + filter_ids = params.get("format_ids") + if filter_ids: + wanted = {(fid.get("agent_url"), fid["id"]) for fid in filter_ids if "id" in fid} + formats = [ + f + for f in all_formats + if (f["format_id"].get("agent_url"), f["format_id"]["id"]) in wanted ] - ) + else: + formats = all_formats + return creative_formats_response(formats) async def sync_creatives(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]: results = [] @@ -414,7 +462,7 @@ async def simulate_delivery( impressions: int | None = None, clicks: int | None = None, conversions: int | None = None, - reported_spend: float | None = None, + reported_spend: dict[str, Any] | None = None, ) -> dict[str, Any]: if media_buy_id not in media_buys: raise TestControllerError("NOT_FOUND", f"Media buy {media_buy_id} not found")