Module adcp.utils.preview_cache
Helper utilities for generating creative preview URLs for grid rendering.
Functions
async def add_preview_urls_to_formats(formats: list[Any],
creative_agent_client: ADCPClient,
use_batch: bool = True,
output_format: str = 'url') ‑> list[dict[str, Any]]-
Expand source code
async def add_preview_urls_to_formats( formats: list[Any], creative_agent_client: ADCPClient, use_batch: bool = True, output_format: str = "url", ) -> list[dict[str, Any]]: """ Add preview URLs to each format by generating sample manifests. Uses batch API for 5-10x better performance when previewing multiple formats. Args: formats: List of Format objects creative_agent_client: Client for the creative agent use_batch: If True, use batch API (default). Set False to use individual requests. output_format: "url" for iframe URLs, "html" for untrusted iframe ``srcdoc`` content Returns: List of format dicts with added preview_data fields """ if not formats: return [] generator = PreviewURLGenerator(creative_agent_client) # Prepare all requests format_requests = [] for fmt in formats: sample_manifest = _create_sample_manifest_for_format(fmt) if sample_manifest: format_requests.append((fmt, sample_manifest)) if not format_requests: return [fmt.model_dump(exclude_none=True) for fmt in formats] # Use batch API if requested and we have multiple formats if use_batch and len(format_requests) > 1: # Batch mode - much faster! batch_requests = [(fmt, manifest) for fmt, manifest in format_requests] preview_data_list = await generator.get_preview_data_batch( batch_requests, output_format=output_format ) # Merge preview data back with formats result = [] preview_idx = 0 for fmt in formats: format_dict = fmt.model_dump(exclude_none=True) # Check if this format had a manifest if preview_idx < len(format_requests) and format_requests[preview_idx][0] == fmt: preview_data = preview_data_list[preview_idx] if preview_data: format_dict["preview_data"] = preview_data preview_idx += 1 result.append(format_dict) return result else: # Fallback to individual requests (for single format or when batch disabled) import asyncio async def process_format(fmt: Format) -> dict[str, Any]: """Process a single format and add preview data.""" format_dict = fmt.model_dump(exclude_none=True) try: sample_manifest = _create_sample_manifest_for_format(fmt) if sample_manifest: preview_data = await generator.get_preview_data_for_manifest( fmt, sample_manifest ) if preview_data: format_dict["preview_data"] = preview_data except Exception as e: logger.warning(f"Failed to add preview data for format {fmt}: {e}") return format_dict return await asyncio.gather(*[process_format(fmt) for fmt in formats])Add preview URLs to each format by generating sample manifests.
Uses batch API for 5-10x better performance when previewing multiple formats.
- Args
- -----=
formats- List of Format objects
creative_agent_client- Client for the creative agent
use_batch- If True, use batch API (default). Set False to use individual requests.
output_format- "url" for iframe URLs, "html" for untrusted iframe
srcdoccontent
Returns -----= List of format dicts with added preview_data fields
async def add_preview_urls_to_products(products: list[Any],
creative_agent_client: ADCPClient,
use_batch: bool = True,
output_format: str = 'url') ‑> list[dict[str, Any]]-
Expand source code
async def add_preview_urls_to_products( products: list[Any], creative_agent_client: ADCPClient, use_batch: bool = True, output_format: str = "url", ) -> list[dict[str, Any]]: """ Add preview URLs to products for their supported formats. Uses batch API for 5-10x better performance when previewing many product formats. Args: products: List of Product objects creative_agent_client: Client for the creative agent use_batch: If True, use batch API (default). Set False to use individual requests. output_format: "url" for iframe URLs, "html" for untrusted iframe ``srcdoc`` content Returns: List of product dicts with added format_previews field """ if not products: return [] generator = PreviewURLGenerator(creative_agent_client) # Collect all unique format_id + manifest combinations across all products all_requests: list[tuple[Product, Format, CreativeManifest]] = [] for product in products: for declaration in product.format_options: sample_manifest = _create_sample_manifest_for_format(declaration) if sample_manifest: all_requests.append((product, declaration, sample_manifest)) if not all_requests: return [p.model_dump(exclude_none=True) for p in products] # Use batch API if requested and we have multiple requests if use_batch and len(all_requests) > 1: # Batch mode - much faster! batch_requests = [(declaration, manifest) for _, declaration, manifest in all_requests] preview_data_list = await generator.get_preview_data_batch( batch_requests, output_format=output_format ) # Map results back to products # Build a mapping from product_id -> format_id -> preview_data product_previews: dict[str, dict[str, dict[str, Any]]] = {} for (product, declaration, _), preview_data in zip(all_requests, preview_data_list): if preview_data: if product.product_id not in product_previews: product_previews[product.product_id] = {} key = declaration.format_option_id or declaration.format_kind product_previews[product.product_id][key] = preview_data # Add preview data to products result = [] for product in products: product_dict = cast(dict[str, Any], product.model_dump(exclude_none=True)) if product.product_id in product_previews: product_dict["format_previews"] = product_previews[product.product_id] result.append(product_dict) return result else: # Fallback to individual requests (for single product/format or when batch disabled) import asyncio async def process_product(product: Product) -> dict[str, Any]: """Process a single product and add preview data for all its formats.""" product_dict = product.model_dump(exclude_none=True) async def process_format(declaration: Format) -> tuple[str, dict[str, Any] | None]: """Process a single format for this product.""" try: sample_manifest = _create_sample_manifest_for_format(declaration) if sample_manifest: preview_data = await generator.get_preview_data_for_manifest( declaration, sample_manifest ) key = declaration.format_option_id or declaration.format_kind return (key, preview_data) except Exception as e: logger.warning( f"Failed to generate preview for product {product.product_id}, " f"format {declaration}: {e}" ) key = declaration.format_option_id or declaration.format_kind return (key, None) format_tasks = [process_format(item) for item in product.format_options] format_results = await asyncio.gather(*format_tasks) format_previews = {fid: data for fid, data in format_results if data is not None} if format_previews: product_dict["format_previews"] = format_previews return product_dict return await asyncio.gather(*[process_product(product) for product in products])Add preview URLs to products for their supported formats.
Uses batch API for 5-10x better performance when previewing many product formats.
- Args
- -----=
products- List of Product objects
creative_agent_client- Client for the creative agent
use_batch- If True, use batch API (default). Set False to use individual requests.
output_format- "url" for iframe URLs, "html" for untrusted iframe
srcdoccontent
Returns -----= List of product dicts with added format_previews field
Classes
class PreviewURLGenerator (creative_agent_client: ADCPClient,
*,
max_cache_entries: int = 256,
max_preview_bytes: int = 1048576)-
Expand source code
class PreviewURLGenerator: """Helper class for generating preview URLs from creative agents.""" def __init__( self, creative_agent_client: ADCPClient, *, max_cache_entries: int = 256, max_preview_bytes: int = 1_048_576, ): """ Initialize preview URL generator. Args: creative_agent_client: ADCPClient configured to talk to a creative agent max_cache_entries: Maximum retained previews; oldest entries are evicted first max_preview_bytes: Maximum serialized size accepted for one preview """ if max_cache_entries < 1: raise ValueError("max_cache_entries must be at least 1") if max_preview_bytes < 1: raise ValueError("max_preview_bytes must be at least 1") self.creative_agent_client = creative_agent_client self.max_cache_entries = max_cache_entries self.max_preview_bytes = max_preview_bytes self._preview_cache: OrderedDict[str, dict[str, Any]] = OrderedDict() @staticmethod def _is_expired(preview_data: dict[str, Any]) -> bool: expires_at = preview_data.get("expires_at") if not isinstance(expires_at, str) or not expires_at: return False try: expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00")) except ValueError: return True if expiry.tzinfo is None: expiry = expiry.replace(tzinfo=timezone.utc) return expiry <= datetime.now(timezone.utc) def _get_cached(self, cache_key: str) -> dict[str, Any] | None: preview_data = self._preview_cache.get(cache_key) if preview_data is None: return None if self._is_expired(preview_data): del self._preview_cache[cache_key] return None self._preview_cache.move_to_end(cache_key) return preview_data def _store_preview(self, cache_key: str, preview_data: dict[str, Any]) -> bool: encoded = json.dumps(preview_data, default=str, ensure_ascii=False).encode("utf-8") if len(encoded) > self.max_preview_bytes or self._is_expired(preview_data): return False self._preview_cache[cache_key] = preview_data self._preview_cache.move_to_end(cache_key) while len(self._preview_cache) > self.max_cache_entries: self._preview_cache.popitem(last=False) return True async def get_preview_data_for_manifest( self, format_id: Any, manifest: CreativeManifest ) -> dict[str, Any] | None: """ Generate preview data for a creative manifest. Returns untrusted preview data plus the mandatory iframe rendering policy. Callers must never inject ``preview_html`` into the host DOM. Args: format_id: Format identifier manifest: Creative manifest Returns: Preview data with preview_url and metadata, or None if generation fails """ from adcp.types.legacy import LegacyPreviewCreativeRequest cache_key = _make_manifest_cache_key(format_id, manifest.model_dump(exclude_none=True)) cached = self._get_cached(cache_key) if cached is not None: return cached try: request_payload: dict[str, Any] = dict( request_type="single", creative_manifest=manifest.model_dump(mode="json", exclude_none=True), ) if hasattr(format_id, "agent_url"): request_payload["format_id"] = format_id request = LegacyPreviewCreativeRequest(**request_payload) result = await self.creative_agent_client.preview_creative_legacy(request) if result.success and result.data and result.data.previews: preview = result.data.previews[0] first_render = preview.renders[0] if preview.renders else None if first_render: render = first_render preview_data = { "preview_id": preview.preview_id, "input": preview.input.model_dump(), "expires_at": ( str(result.data.expires_at) if result.data.expires_at is not None else None ), **_preview_render_data(render), } if not self._store_preview(cache_key, preview_data): logger.warning( "Preview rejected because it is oversized or already expired" ) return None return preview_data except Exception as e: logger.warning(f"Failed to generate preview for format {format_id}: {e}", exc_info=True) return None async def get_preview_data_batch( self, requests: list[tuple[Any, CreativeManifest]], output_format: str = "url", ) -> list[dict[str, Any] | None]: """ Generate preview data for multiple manifests in one API call (batch mode). This is 5-10x faster than individual requests for multiple previews. Args: requests: List of (format_id, manifest) tuples to preview output_format: "url" for iframe URLs, "html" for untrusted iframe ``srcdoc`` content Returns: List of preview data dicts (or None for failures), in same order as requests """ from pydantic import TypeAdapter from adcp.types.legacy import LegacyPreviewCreativeRequest _pcr_adapter: TypeAdapter[Any] = TypeAdapter(LegacyPreviewCreativeRequest) if not requests: return [] # Check cache first cache_keys = [ _make_manifest_cache_key( fid, manifest.model_dump(exclude_none=True), output_format=output_format ) for fid, manifest in requests ] # Separate cached vs uncached requests uncached_indices: list[int] = [] uncached_requests: list[dict[str, Any]] = [] results: list[dict[str, Any] | None] = [None] * len(requests) for idx, (cache_key, (format_id, manifest)) in enumerate(zip(cache_keys, requests)): cached = self._get_cached(cache_key) if cached is not None: results[idx] = cached else: uncached_indices.append(idx) entry = {"creative_manifest": manifest.model_dump(exclude_none=True)} if hasattr(format_id, "agent_url"): entry["format_id"] = format_id.model_dump(mode="json") uncached_requests.append(entry) # If everything was cached, return early if not uncached_requests: return results # Make batch API call for uncached items try: # Batch requests in chunks of 50 (API limit) batch_size = 50 for chunk_start in range(0, len(uncached_requests), batch_size): chunk_end = min(chunk_start + batch_size, len(uncached_requests)) chunk_requests = uncached_requests[chunk_start:chunk_end] chunk_indices = uncached_indices[chunk_start:chunk_end] batch_request = _pcr_adapter.validate_python( { "request_type": "batch", "requests": chunk_requests, "output_format": output_format, "context": None, } ) result = await self.creative_agent_client.preview_creative_legacy(batch_request) if result.success and result.data and result.data.results: # Process batch results for result_idx, batch_result in enumerate(result.data.results): batch_result = ( batch_result if isinstance(batch_result, dict) else batch_result.model_dump(mode="json", exclude_none=True) ) original_idx = chunk_indices[result_idx] cache_key = cache_keys[original_idx] if batch_result.get("success") and batch_result.get("response"): response = batch_result["response"] if response.get("previews"): preview = response["previews"][0] renders = preview.get("renders", []) first_render = renders[0] if renders else {} preview_data = { "preview_id": preview.get("preview_id"), "input": preview.get("input", {}), "expires_at": response.get("expires_at"), **_preview_render_data(first_render), } if self._store_preview(cache_key, preview_data): results[original_idx] = preview_data else: logger.warning( "Batch preview %s rejected because it is oversized " "or already expired", original_idx, ) else: # Request failed error = batch_result.get("error", {}) logger.warning( f"Batch preview failed for request {original_idx}: " f"{error.get('message', 'Unknown error')}" ) except Exception as e: logger.warning(f"Batch preview generation failed: {e}", exc_info=True) return resultsHelper class for generating preview URLs from creative agents.
Initialize preview URL generator.
- Args
- -----=
creative_agent_client- ADCPClient configured to talk to a creative agent
max_cache_entries- Maximum retained previews; oldest entries are evicted first
max_preview_bytes- Maximum serialized size accepted for one preview
Methods
async def get_preview_data_batch(self, requests: list[tuple[Any, CreativeManifest]], output_format: str = 'url') ‑> list[dict[str, Any] | None]-
Expand source code
async def get_preview_data_batch( self, requests: list[tuple[Any, CreativeManifest]], output_format: str = "url", ) -> list[dict[str, Any] | None]: """ Generate preview data for multiple manifests in one API call (batch mode). This is 5-10x faster than individual requests for multiple previews. Args: requests: List of (format_id, manifest) tuples to preview output_format: "url" for iframe URLs, "html" for untrusted iframe ``srcdoc`` content Returns: List of preview data dicts (or None for failures), in same order as requests """ from pydantic import TypeAdapter from adcp.types.legacy import LegacyPreviewCreativeRequest _pcr_adapter: TypeAdapter[Any] = TypeAdapter(LegacyPreviewCreativeRequest) if not requests: return [] # Check cache first cache_keys = [ _make_manifest_cache_key( fid, manifest.model_dump(exclude_none=True), output_format=output_format ) for fid, manifest in requests ] # Separate cached vs uncached requests uncached_indices: list[int] = [] uncached_requests: list[dict[str, Any]] = [] results: list[dict[str, Any] | None] = [None] * len(requests) for idx, (cache_key, (format_id, manifest)) in enumerate(zip(cache_keys, requests)): cached = self._get_cached(cache_key) if cached is not None: results[idx] = cached else: uncached_indices.append(idx) entry = {"creative_manifest": manifest.model_dump(exclude_none=True)} if hasattr(format_id, "agent_url"): entry["format_id"] = format_id.model_dump(mode="json") uncached_requests.append(entry) # If everything was cached, return early if not uncached_requests: return results # Make batch API call for uncached items try: # Batch requests in chunks of 50 (API limit) batch_size = 50 for chunk_start in range(0, len(uncached_requests), batch_size): chunk_end = min(chunk_start + batch_size, len(uncached_requests)) chunk_requests = uncached_requests[chunk_start:chunk_end] chunk_indices = uncached_indices[chunk_start:chunk_end] batch_request = _pcr_adapter.validate_python( { "request_type": "batch", "requests": chunk_requests, "output_format": output_format, "context": None, } ) result = await self.creative_agent_client.preview_creative_legacy(batch_request) if result.success and result.data and result.data.results: # Process batch results for result_idx, batch_result in enumerate(result.data.results): batch_result = ( batch_result if isinstance(batch_result, dict) else batch_result.model_dump(mode="json", exclude_none=True) ) original_idx = chunk_indices[result_idx] cache_key = cache_keys[original_idx] if batch_result.get("success") and batch_result.get("response"): response = batch_result["response"] if response.get("previews"): preview = response["previews"][0] renders = preview.get("renders", []) first_render = renders[0] if renders else {} preview_data = { "preview_id": preview.get("preview_id"), "input": preview.get("input", {}), "expires_at": response.get("expires_at"), **_preview_render_data(first_render), } if self._store_preview(cache_key, preview_data): results[original_idx] = preview_data else: logger.warning( "Batch preview %s rejected because it is oversized " "or already expired", original_idx, ) else: # Request failed error = batch_result.get("error", {}) logger.warning( f"Batch preview failed for request {original_idx}: " f"{error.get('message', 'Unknown error')}" ) except Exception as e: logger.warning(f"Batch preview generation failed: {e}", exc_info=True) return resultsGenerate preview data for multiple manifests in one API call (batch mode).
This is 5-10x faster than individual requests for multiple previews.
- Args
- -----=
requests- List of (format_id, manifest) tuples to preview
output_format- "url" for iframe URLs, "html" for untrusted iframe
srcdoccontent
Returns -----= List of preview data dicts (or None for failures), in same order as requests
async def get_preview_data_for_manifest(self, format_id: Any, manifest: CreativeManifest) ‑> dict[str, Any] | None-
Expand source code
async def get_preview_data_for_manifest( self, format_id: Any, manifest: CreativeManifest ) -> dict[str, Any] | None: """ Generate preview data for a creative manifest. Returns untrusted preview data plus the mandatory iframe rendering policy. Callers must never inject ``preview_html`` into the host DOM. Args: format_id: Format identifier manifest: Creative manifest Returns: Preview data with preview_url and metadata, or None if generation fails """ from adcp.types.legacy import LegacyPreviewCreativeRequest cache_key = _make_manifest_cache_key(format_id, manifest.model_dump(exclude_none=True)) cached = self._get_cached(cache_key) if cached is not None: return cached try: request_payload: dict[str, Any] = dict( request_type="single", creative_manifest=manifest.model_dump(mode="json", exclude_none=True), ) if hasattr(format_id, "agent_url"): request_payload["format_id"] = format_id request = LegacyPreviewCreativeRequest(**request_payload) result = await self.creative_agent_client.preview_creative_legacy(request) if result.success and result.data and result.data.previews: preview = result.data.previews[0] first_render = preview.renders[0] if preview.renders else None if first_render: render = first_render preview_data = { "preview_id": preview.preview_id, "input": preview.input.model_dump(), "expires_at": ( str(result.data.expires_at) if result.data.expires_at is not None else None ), **_preview_render_data(render), } if not self._store_preview(cache_key, preview_data): logger.warning( "Preview rejected because it is oversized or already expired" ) return None return preview_data except Exception as e: logger.warning(f"Failed to generate preview for format {format_id}: {e}", exc_info=True) return NoneGenerate preview data for a creative manifest.
Returns untrusted preview data plus the mandatory iframe rendering policy. Callers must never inject
preview_htmlinto the host DOM.- Args
- -----=
format_id- Format identifier
manifest- Creative manifest
Returns -----= Preview data with preview_url and metadata, or None if generation fails