adcp 1.0.5__py3-none-any.whl → 1.1.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/__init__.py CHANGED
@@ -46,7 +46,7 @@ from adcp.types.generated import (
46
46
  UpdateMediaBuyResponse,
47
47
  )
48
48
 
49
- __version__ = "1.0.5"
49
+ __version__ = "1.1.0"
50
50
 
51
51
  __all__ = [
52
52
  # Client classes
adcp/client.py CHANGED
@@ -37,6 +37,8 @@ from adcp.types.generated import (
37
37
  ListCreativeFormatsResponse,
38
38
  ListCreativesRequest,
39
39
  ListCreativesResponse,
40
+ PreviewCreativeRequest,
41
+ PreviewCreativeResponse,
40
42
  ProvidePerformanceFeedbackRequest,
41
43
  ProvidePerformanceFeedbackResponse,
42
44
  SyncCreativesRequest,
@@ -101,16 +103,31 @@ class ADCPClient:
101
103
  async def get_products(
102
104
  self,
103
105
  request: GetProductsRequest,
106
+ fetch_previews: bool = False,
107
+ preview_output_format: str = "url",
108
+ creative_agent_client: ADCPClient | None = None,
104
109
  ) -> TaskResult[GetProductsResponse]:
105
110
  """
106
111
  Get advertising products.
107
112
 
108
113
  Args:
109
114
  request: Request parameters
115
+ fetch_previews: If True, generate preview URLs for each product's formats
116
+ (uses batch API for 5-10x performance improvement)
117
+ preview_output_format: "url" for iframe URLs (default), "html" for direct
118
+ embedding (2-3x faster, no iframe overhead)
119
+ creative_agent_client: Client for creative agent (required if
120
+ fetch_previews=True)
110
121
 
111
122
  Returns:
112
- TaskResult containing GetProductsResponse
123
+ TaskResult containing GetProductsResponse with optional preview URLs in metadata
124
+
125
+ Raises:
126
+ ValueError: If fetch_previews=True but creative_agent_client is not provided
113
127
  """
128
+ if fetch_previews and not creative_agent_client:
129
+ raise ValueError("creative_agent_client is required when fetch_previews=True")
130
+
114
131
  operation_id = create_operation_id()
115
132
  params = request.model_dump(exclude_none=True)
116
133
 
@@ -137,20 +154,40 @@ class ADCPClient:
137
154
  )
138
155
  )
139
156
 
140
- return self.adapter._parse_response(raw_result, GetProductsResponse)
157
+ result = self.adapter._parse_response(raw_result, GetProductsResponse)
158
+
159
+ if fetch_previews and result.success and result.data and creative_agent_client:
160
+ from adcp.utils.preview_cache import add_preview_urls_to_products
161
+
162
+ products_with_previews = await add_preview_urls_to_products(
163
+ result.data.products,
164
+ creative_agent_client,
165
+ use_batch=True,
166
+ output_format=preview_output_format,
167
+ )
168
+ result.metadata = result.metadata or {}
169
+ result.metadata["products_with_previews"] = products_with_previews
170
+
171
+ return result
141
172
 
142
173
  async def list_creative_formats(
143
174
  self,
144
175
  request: ListCreativeFormatsRequest,
176
+ fetch_previews: bool = False,
177
+ preview_output_format: str = "url",
145
178
  ) -> TaskResult[ListCreativeFormatsResponse]:
146
179
  """
147
180
  List supported creative formats.
148
181
 
149
182
  Args:
150
183
  request: Request parameters
184
+ fetch_previews: If True, generate preview URLs for each format using
185
+ sample manifests (uses batch API for 5-10x performance improvement)
186
+ preview_output_format: "url" for iframe URLs (default), "html" for direct
187
+ embedding (2-3x faster, no iframe overhead)
151
188
 
152
189
  Returns:
153
- TaskResult containing ListCreativeFormatsResponse
190
+ TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata
154
191
  """
155
192
  operation_id = create_operation_id()
156
193
  params = request.model_dump(exclude_none=True)
@@ -178,8 +215,62 @@ class ADCPClient:
178
215
  )
179
216
  )
180
217
 
181
- # Parse response using adapter's helper
182
- return self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
218
+ result = self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
219
+
220
+ if fetch_previews and result.success and result.data:
221
+ from adcp.utils.preview_cache import add_preview_urls_to_formats
222
+
223
+ formats_with_previews = await add_preview_urls_to_formats(
224
+ result.data.formats,
225
+ self,
226
+ use_batch=True,
227
+ output_format=preview_output_format,
228
+ )
229
+ result.metadata = result.metadata or {}
230
+ result.metadata["formats_with_previews"] = formats_with_previews
231
+
232
+ return result
233
+
234
+ async def preview_creative(
235
+ self,
236
+ request: PreviewCreativeRequest,
237
+ ) -> TaskResult[PreviewCreativeResponse]:
238
+ """
239
+ Generate preview of a creative manifest.
240
+
241
+ Args:
242
+ request: Request parameters
243
+
244
+ Returns:
245
+ TaskResult containing PreviewCreativeResponse with preview URLs
246
+ """
247
+ operation_id = create_operation_id()
248
+ params = request.model_dump(exclude_none=True)
249
+
250
+ self._emit_activity(
251
+ Activity(
252
+ type=ActivityType.PROTOCOL_REQUEST,
253
+ operation_id=operation_id,
254
+ agent_id=self.agent_config.id,
255
+ task_type="preview_creative",
256
+ timestamp=datetime.now(timezone.utc).isoformat(),
257
+ )
258
+ )
259
+
260
+ raw_result = await self.adapter.preview_creative(params) # type: ignore[attr-defined]
261
+
262
+ self._emit_activity(
263
+ Activity(
264
+ type=ActivityType.PROTOCOL_RESPONSE,
265
+ operation_id=operation_id,
266
+ agent_id=self.agent_config.id,
267
+ task_type="preview_creative",
268
+ status=raw_result.status,
269
+ timestamp=datetime.now(timezone.utc).isoformat(),
270
+ )
271
+ )
272
+
273
+ return self.adapter._parse_response(raw_result, PreviewCreativeResponse)
183
274
 
184
275
  async def sync_creatives(
185
276
  self,
adcp/protocols/a2a.py CHANGED
@@ -244,6 +244,10 @@ class A2AAdapter(ProtocolAdapter):
244
244
  """Provide performance feedback."""
245
245
  return await self._call_a2a_tool("provide_performance_feedback", params)
246
246
 
247
+ async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
248
+ """Generate preview URLs for a creative manifest."""
249
+ return await self._call_a2a_tool("preview_creative", params)
250
+
247
251
  async def list_tools(self) -> list[str]:
248
252
  """
249
253
  List available tools from A2A agent.
adcp/protocols/mcp.py CHANGED
@@ -40,6 +40,39 @@ class MCPAdapter(ProtocolAdapter):
40
40
  self._session: Any = None
41
41
  self._exit_stack: Any = None
42
42
 
43
+ async def _cleanup_failed_connection(self, context: str) -> None:
44
+ """
45
+ Clean up resources after a failed connection attempt.
46
+
47
+ This method handles cleanup without raising exceptions to avoid
48
+ masking the original connection error.
49
+
50
+ Args:
51
+ context: Description of the context for logging (e.g., "during connection attempt")
52
+ """
53
+ if self._exit_stack is not None:
54
+ old_stack = self._exit_stack
55
+ self._exit_stack = None
56
+ self._session = None
57
+ try:
58
+ await old_stack.aclose()
59
+ except asyncio.CancelledError:
60
+ logger.debug(f"MCP session cleanup cancelled {context}")
61
+ except RuntimeError as cleanup_error:
62
+ # Known anyio task group cleanup issue
63
+ error_msg = str(cleanup_error).lower()
64
+ if "cancel scope" in error_msg or "async context" in error_msg:
65
+ logger.debug(f"Ignoring anyio cleanup error {context}: {cleanup_error}")
66
+ else:
67
+ logger.warning(
68
+ f"Unexpected RuntimeError during cleanup {context}: {cleanup_error}"
69
+ )
70
+ except Exception as cleanup_error:
71
+ # Log unexpected cleanup errors but don't raise to preserve original error
72
+ logger.warning(
73
+ f"Unexpected error during cleanup {context}: {cleanup_error}", exc_info=True
74
+ )
75
+
43
76
  async def _get_session(self) -> ClientSession:
44
77
  """
45
78
  Get or create MCP client session with URL fallback handling.
@@ -115,35 +148,8 @@ class MCPAdapter(ProtocolAdapter):
115
148
  return self._session # type: ignore[no-any-return]
116
149
  except Exception as e:
117
150
  last_error = e
118
- # Clean up the exit stack on failure to avoid async scope issues
119
- if self._exit_stack is not None:
120
- old_stack = self._exit_stack
121
- self._exit_stack = None # Clear immediately to prevent reuse
122
- self._session = None
123
- try:
124
- await old_stack.aclose()
125
- except asyncio.CancelledError:
126
- # Expected during shutdown
127
- pass
128
- except RuntimeError as cleanup_error:
129
- # Known MCP SDK async cleanup issue
130
- if (
131
- "async context" in str(cleanup_error).lower()
132
- or "cancel scope" in str(cleanup_error).lower()
133
- ):
134
- logger.debug(
135
- "Ignoring MCP SDK async context error during cleanup: "
136
- f"{cleanup_error}"
137
- )
138
- else:
139
- logger.warning(
140
- f"Unexpected RuntimeError during cleanup: {cleanup_error}"
141
- )
142
- except Exception as cleanup_error:
143
- # Unexpected cleanup errors should be logged
144
- logger.warning(
145
- f"Unexpected error during cleanup: {cleanup_error}", exc_info=True
146
- )
151
+ # Clean up the exit stack on failure to avoid resource leaks
152
+ await self._cleanup_failed_connection("during connection attempt")
147
153
 
148
154
  # If this isn't the last URL to try, create a new exit stack and continue
149
155
  if url != urls_to_try[-1]:
@@ -341,6 +347,10 @@ class MCPAdapter(ProtocolAdapter):
341
347
  """Provide performance feedback."""
342
348
  return await self._call_mcp_tool("provide_performance_feedback", params)
343
349
 
350
+ async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
351
+ """Generate preview URLs for a creative manifest."""
352
+ return await self._call_mcp_tool("preview_creative", params)
353
+
344
354
  async def list_tools(self) -> list[str]:
345
355
  """List available tools from MCP agent."""
346
356
  session = await self._get_session()
@@ -348,15 +358,5 @@ class MCPAdapter(ProtocolAdapter):
348
358
  return [tool.name for tool in result.tools]
349
359
 
350
360
  async def close(self) -> None:
351
- """Close the MCP session."""
352
- if self._exit_stack is not None:
353
- old_stack = self._exit_stack
354
- self._exit_stack = None
355
- self._session = None
356
- try:
357
- await old_stack.aclose()
358
- except (asyncio.CancelledError, RuntimeError):
359
- # Cleanup errors during shutdown are expected
360
- pass
361
- except Exception as e:
362
- logger.debug(f"Error during MCP session cleanup: {e}")
361
+ """Close the MCP session and clean up resources."""
362
+ await self._cleanup_failed_connection("during close")
adcp/types/core.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from enum import Enum
6
6
  from typing import Any, Generic, Literal, TypeVar
7
7
 
8
- from pydantic import BaseModel, Field, field_validator
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
9
 
10
10
 
11
11
  class Protocol(str, Enum):
@@ -125,6 +125,8 @@ class DebugInfo(BaseModel):
125
125
  class TaskResult(BaseModel, Generic[T]):
126
126
  """Result from task execution."""
127
127
 
128
+ model_config = ConfigDict(arbitrary_types_allowed=True)
129
+
128
130
  status: TaskStatus
129
131
  data: T | None = None
130
132
  message: str | None = None # Human-readable message from agent (e.g., MCP content text)
@@ -135,9 +137,6 @@ class TaskResult(BaseModel, Generic[T]):
135
137
  metadata: dict[str, Any] | None = None
136
138
  debug_info: DebugInfo | None = None
137
139
 
138
- class Config:
139
- arbitrary_types_allowed = True
140
-
141
140
 
142
141
  class ActivityType(str, Enum):
143
142
  """Types of activity events."""
adcp/types/generated.py CHANGED
@@ -32,23 +32,6 @@ ReportingCapabilities = dict[str, Any]
32
32
  # CORE DOMAIN TYPES
33
33
  # ============================================================================
34
34
 
35
- class FormatId(BaseModel):
36
- """Structured format identifier with agent URL and format name"""
37
-
38
- agent_url: str = Field(description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)")
39
- id: str = Field(description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')")
40
-
41
- @field_validator("id")
42
- @classmethod
43
- def validate_id_pattern(cls, v: str) -> str:
44
- """Validate format ID contains only alphanumeric characters, hyphens, and underscores."""
45
- if not re.match(r"^[a-zA-Z0-9_-]+$", v):
46
- raise ValueError(
47
- f"Invalid format ID: {v!r}. Must contain only alphanumeric characters, hyphens, and underscores"
48
- )
49
- return v
50
-
51
-
52
35
  class Product(BaseModel):
53
36
  """Represents available advertising inventory"""
54
37
 
@@ -68,6 +51,8 @@ class Product(BaseModel):
68
51
  is_custom: bool | None = Field(None, description="Whether this is a custom product")
69
52
  brief_relevance: str | None = Field(None, description="Explanation of why this product matches the brief (only included when brief is provided)")
70
53
  expires_at: str | None = Field(None, description="Expiration timestamp for custom products")
54
+ product_card: dict[str, Any] | None = Field(None, description="Optional standard visual card (300x400px) for displaying this product in user interfaces. Can be rendered via preview_creative or pre-generated.")
55
+ product_card_detailed: dict[str, Any] | None = Field(None, description="Optional detailed card with carousel and full specifications. Provides rich product presentation similar to media kit pages.")
71
56
 
72
57
 
73
58
  class MediaBuy(BaseModel):
@@ -151,7 +136,7 @@ class Format(BaseModel):
151
136
  format_id: FormatId = Field(description="Structured format identifier with agent URL and format name")
152
137
  name: str = Field(description="Human-readable format name")
153
138
  description: str | None = Field(None, description="Plain text explanation of what this format does and what assets it requires")
154
- preview_image: str | None = Field(None, description="Optional preview image URL for format browsing/discovery UI. Should be 400x300px (4:3 aspect ratio) PNG or JPG. Used as thumbnail/card image in format browsers.")
139
+ preview_image: str | None = Field(None, description="DEPRECATED: Use format_card instead. Optional preview image URL for format browsing/discovery UI. Should be 400x300px (4:3 aspect ratio) PNG or JPG. Used as thumbnail/card image in format browsers. This field is maintained for backward compatibility but format_card provides a more flexible, structured approach.")
155
140
  example_url: str | None = Field(None, description="Optional URL to showcase page with examples and interactive demos of this format")
156
141
  type: Literal["audio", "video", "display", "native", "dooh", "rich_media", "universal"] = Field(description="Media type of this format - determines rendering method and asset requirements")
157
142
  renders: list[dict[str, Any]] | None = Field(None, description="Specification of rendered pieces for this format. Most formats produce a single render. Companion ad formats (video + banner), adaptive formats, and multi-placement formats produce multiple renders. Each render specifies its role and dimensions.")
@@ -159,6 +144,8 @@ class Format(BaseModel):
159
144
  delivery: dict[str, Any] | None = Field(None, description="Delivery method specifications (e.g., hosted, VAST, third-party tags)")
160
145
  supported_macros: list[str] | None = Field(None, description="List of universal macros supported by this format (e.g., MEDIA_BUY_ID, CACHEBUSTER, DEVICE_ID). Used for validation and developer tooling.")
161
146
  output_format_ids: list[FormatId] | None = Field(None, description="For generative formats: array of format IDs that this format can generate. When a format accepts inputs like brand_manifest and message, this specifies what concrete output formats can be produced (e.g., a generative banner format might output standard image banner formats).")
147
+ format_card: dict[str, Any] | None = Field(None, description="Optional standard visual card (300x400px) for displaying this format in user interfaces. Can be rendered via preview_creative or pre-generated.")
148
+ format_card_detailed: dict[str, Any] | None = Field(None, description="Optional detailed card with carousel and full specifications. Provides rich format documentation similar to ad spec pages.")
162
149
 
163
150
 
164
151
  class Targeting(BaseModel):
@@ -469,15 +456,6 @@ class ListCreativesRequest(BaseModel):
469
456
  fields: list[Literal["creative_id", "name", "format", "status", "created_date", "updated_date", "tags", "assignments", "performance", "sub_assets"]] | None = Field(None, description="Specific fields to include in response (omit for all fields)")
470
457
 
471
458
 
472
- class PreviewCreativeRequest(BaseModel):
473
- """Request to generate a preview of a creative manifest in a specific format. The creative_manifest should include all assets required by the format (e.g., promoted_offerings for generative formats)."""
474
-
475
- format_id: FormatId = Field(description="Format identifier for rendering the preview")
476
- creative_manifest: CreativeManifest = Field(description="Complete creative manifest with all required assets (including promoted_offerings if required by the format)")
477
- inputs: list[dict[str, Any]] | None = Field(None, description="Array of input sets for generating multiple preview variants. Each input set defines macros and context values for one preview rendering. If not provided, creative agent will generate default previews.")
478
- template_id: str | None = Field(None, description="Specific template ID for custom format rendering")
479
-
480
-
481
459
  class ProvidePerformanceFeedbackRequest(BaseModel):
482
460
  """Request payload for provide_performance_feedback task"""
483
461
 
@@ -599,14 +577,6 @@ class ListCreativesResponse(BaseModel):
599
577
  status_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by status")
600
578
 
601
579
 
602
- class PreviewCreativeResponse(BaseModel):
603
- """Response containing preview links for a creative. Each preview URL returns an HTML page that can be embedded in an iframe to display the rendered creative."""
604
-
605
- previews: list[dict[str, Any]] = Field(description="Array of preview variants. Each preview corresponds to an input set from the request. If no inputs were provided, returns a single default preview.")
606
- interactive_url: str | None = Field(None, description="Optional URL to an interactive testing page that shows all preview variants with controls to switch between them, modify macro values, and test different scenarios.")
607
- expires_at: str = Field(description="ISO 8601 timestamp when preview links expire")
608
-
609
-
610
580
  class ProvidePerformanceFeedbackResponse(BaseModel):
611
581
  """Response payload for provide_performance_feedback task"""
612
582
 
@@ -630,3 +600,56 @@ class UpdateMediaBuyResponse(BaseModel):
630
600
  affected_packages: list[dict[str, Any]] | None = Field(None, description="Array of packages that were modified")
631
601
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., partial update failures)")
632
602
 
603
+
604
+
605
+ # ============================================================================
606
+ # CUSTOM IMPLEMENTATIONS (override type aliases from generator)
607
+ # ============================================================================
608
+ # The simple code generator produces type aliases (e.g., PreviewCreativeRequest = Any)
609
+ # for complex schemas that use oneOf. We override them here with proper Pydantic classes
610
+ # to maintain type safety and enable batch API support.
611
+
612
+
613
+ class FormatId(BaseModel):
614
+ """Structured format identifier with agent URL and format name"""
615
+
616
+ agent_url: str = Field(description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)")
617
+ id: str = Field(description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')")
618
+
619
+ @field_validator("id")
620
+ @classmethod
621
+ def validate_id_pattern(cls, v: str) -> str:
622
+ """Validate format ID contains only alphanumeric characters, hyphens, and underscores."""
623
+ if not re.match(r"^[a-zA-Z0-9_-]+$", v):
624
+ raise ValueError(
625
+ f"Invalid format ID: {v!r}. Must contain only alphanumeric characters, hyphens, and underscores"
626
+ )
627
+ return v
628
+
629
+
630
+ class PreviewCreativeRequest(BaseModel):
631
+ """Request to generate a preview of a creative manifest. Supports single or batch mode."""
632
+
633
+ # Single mode fields
634
+ format_id: FormatId | None = Field(default=None, description="Format identifier for rendering the preview (single mode)")
635
+ creative_manifest: CreativeManifest | None = Field(default=None, description="Complete creative manifest with all required assets (single mode)")
636
+ inputs: list[dict[str, Any]] | None = Field(default=None, description="Array of input sets for generating multiple preview variants")
637
+ template_id: str | None = Field(default=None, description="Specific template ID for custom format rendering")
638
+
639
+ # Batch mode field
640
+ requests: list[dict[str, Any]] | None = Field(default=None, description="Array of preview requests for batch processing (1-50 items)")
641
+
642
+ # Output format (applies to both modes)
643
+ output_format: Literal["url", "html"] | None = Field(default="url", description="Output format: 'url' for iframe URLs, 'html' for direct embedding")
644
+
645
+
646
+ class PreviewCreativeResponse(BaseModel):
647
+ """Response containing preview links for one or more creatives. Format matches the request: single preview response for single requests, batch results for batch requests."""
648
+
649
+ # Single mode fields
650
+ previews: list[dict[str, Any]] | None = Field(default=None, description="Array of preview variants (single mode)")
651
+ interactive_url: str | None = Field(default=None, description="Optional URL to interactive testing page (single mode)")
652
+ expires_at: str | None = Field(default=None, description="ISO 8601 timestamp when preview links expire (single mode)")
653
+
654
+ # Batch mode field
655
+ results: list[dict[str, Any]] | None = Field(default=None, description="Array of preview results for batch processing")
@@ -0,0 +1,461 @@
1
+ """Helper utilities for generating creative preview URLs for grid rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from adcp.client import ADCPClient
11
+ from adcp.types.generated import CreativeManifest, Format, FormatId, Product
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _make_manifest_cache_key(format_id: FormatId | str, manifest_dict: dict[str, Any]) -> str:
17
+ """
18
+ Create a cache key for a format_id and manifest.
19
+
20
+ Args:
21
+ format_id: Format identifier (FormatId object or string)
22
+ manifest_dict: Creative manifest dict
23
+
24
+ Returns:
25
+ Cache key string
26
+ """
27
+ # Convert FormatId to string representation
28
+ if isinstance(format_id, str):
29
+ format_id_str = format_id
30
+ else:
31
+ # FormatId is a Pydantic model with agent_url and id
32
+ format_id_str = f"{format_id.agent_url}:{format_id.id}"
33
+
34
+ manifest_str = str(sorted(manifest_dict.items()))
35
+ combined = f"{format_id_str}:{manifest_str}"
36
+ return hashlib.sha256(combined.encode()).hexdigest()[:16]
37
+
38
+
39
+ class PreviewURLGenerator:
40
+ """Helper class for generating preview URLs from creative agents."""
41
+
42
+ def __init__(self, creative_agent_client: ADCPClient):
43
+ """
44
+ Initialize preview URL generator.
45
+
46
+ Args:
47
+ creative_agent_client: ADCPClient configured to talk to a creative agent
48
+ """
49
+ self.creative_agent_client = creative_agent_client
50
+ self._preview_cache: dict[str, dict[str, Any]] = {}
51
+
52
+ async def get_preview_data_for_manifest(
53
+ self, format_id: FormatId, manifest: CreativeManifest
54
+ ) -> dict[str, Any] | None:
55
+ """
56
+ Generate preview data for a creative manifest.
57
+
58
+ Returns preview data with URLs suitable for embedding in
59
+ <rendered-creative> web components or iframes.
60
+
61
+ Args:
62
+ format_id: Format identifier
63
+ manifest: Creative manifest
64
+
65
+ Returns:
66
+ Preview data with preview_url and metadata, or None if generation fails
67
+ """
68
+ from adcp.types.generated import PreviewCreativeRequest
69
+
70
+ cache_key = _make_manifest_cache_key(format_id, manifest.model_dump(exclude_none=True))
71
+
72
+ if cache_key in self._preview_cache:
73
+ return self._preview_cache[cache_key]
74
+
75
+ try:
76
+ request = PreviewCreativeRequest(
77
+ format_id=format_id, creative_manifest=manifest, inputs=None, template_id=None
78
+ )
79
+ result = await self.creative_agent_client.preview_creative(request)
80
+
81
+ if result.success and result.data and result.data.previews:
82
+ preview = result.data.previews[0]
83
+ renders = preview.get("renders", [])
84
+ first_render = renders[0] if renders else {}
85
+
86
+ preview_data = {
87
+ "preview_id": preview.get("preview_id"),
88
+ "preview_url": first_render.get("preview_url"),
89
+ "preview_html": first_render.get("preview_html"),
90
+ "render_id": first_render.get("render_id"),
91
+ "input": preview.get("input", {}),
92
+ "expires_at": result.data.expires_at,
93
+ }
94
+
95
+ self._preview_cache[cache_key] = preview_data
96
+ return preview_data
97
+
98
+ except Exception as e:
99
+ logger.warning(f"Failed to generate preview for format {format_id}: {e}", exc_info=True)
100
+
101
+ return None
102
+
103
+ async def get_preview_data_batch(
104
+ self,
105
+ requests: list[tuple[FormatId, CreativeManifest]],
106
+ output_format: str = "url",
107
+ ) -> list[dict[str, Any] | None]:
108
+ """
109
+ Generate preview data for multiple manifests in one API call (batch mode).
110
+
111
+ This is 5-10x faster than individual requests for multiple previews.
112
+
113
+ Args:
114
+ requests: List of (format_id, manifest) tuples to preview
115
+ output_format: "url" for iframe URLs, "html" for direct embedding
116
+
117
+ Returns:
118
+ List of preview data dicts (or None for failures), in same order as requests
119
+ """
120
+ from adcp.types.generated import PreviewCreativeRequest
121
+
122
+ if not requests:
123
+ return []
124
+
125
+ # Check cache first
126
+ cache_keys = [
127
+ _make_manifest_cache_key(fid, manifest.model_dump(exclude_none=True))
128
+ for fid, manifest in requests
129
+ ]
130
+
131
+ # Separate cached vs uncached requests
132
+ uncached_indices: list[int] = []
133
+ uncached_requests: list[dict[str, Any]] = []
134
+ results: list[dict[str, Any] | None] = [None] * len(requests)
135
+
136
+ for idx, (cache_key, (format_id, manifest)) in enumerate(zip(cache_keys, requests)):
137
+ if cache_key in self._preview_cache:
138
+ results[idx] = self._preview_cache[cache_key]
139
+ else:
140
+ uncached_indices.append(idx)
141
+ fid_dict = (
142
+ format_id.model_dump()
143
+ if hasattr(format_id, "model_dump")
144
+ else format_id
145
+ )
146
+ uncached_requests.append({
147
+ "format_id": fid_dict,
148
+ "creative_manifest": manifest.model_dump(exclude_none=True),
149
+ })
150
+
151
+ # If everything was cached, return early
152
+ if not uncached_requests:
153
+ return results
154
+
155
+ # Make batch API call for uncached items
156
+ try:
157
+ # Batch requests in chunks of 50 (API limit)
158
+ batch_size = 50
159
+ for chunk_start in range(0, len(uncached_requests), batch_size):
160
+ chunk_end = min(chunk_start + batch_size, len(uncached_requests))
161
+ chunk_requests = uncached_requests[chunk_start:chunk_end]
162
+ chunk_indices = uncached_indices[chunk_start:chunk_end]
163
+
164
+ batch_request = PreviewCreativeRequest(
165
+ requests=chunk_requests,
166
+ output_format=output_format, # type: ignore[arg-type]
167
+ )
168
+ result = await self.creative_agent_client.preview_creative(batch_request)
169
+
170
+ if result.success and result.data and result.data.results:
171
+ # Process batch results
172
+ for result_idx, batch_result in enumerate(result.data.results):
173
+ original_idx = chunk_indices[result_idx]
174
+ cache_key = cache_keys[original_idx]
175
+
176
+ if batch_result.get("success") and batch_result.get("response"):
177
+ response = batch_result["response"]
178
+ if response.get("previews"):
179
+ preview = response["previews"][0]
180
+ renders = preview.get("renders", [])
181
+ first_render = renders[0] if renders else {}
182
+ preview_data = {
183
+ "preview_id": preview.get("preview_id"),
184
+ "preview_url": first_render.get("preview_url"),
185
+ "preview_html": first_render.get("preview_html"),
186
+ "render_id": first_render.get("render_id"),
187
+ "input": preview.get("input", {}),
188
+ "expires_at": response.get("expires_at"),
189
+ }
190
+ # Cache and store
191
+ self._preview_cache[cache_key] = preview_data
192
+ results[original_idx] = preview_data
193
+ else:
194
+ # Request failed
195
+ error = batch_result.get("error", {})
196
+ logger.warning(
197
+ f"Batch preview failed for request {original_idx}: "
198
+ f"{error.get('message', 'Unknown error')}"
199
+ )
200
+
201
+ except Exception as e:
202
+ logger.warning(f"Batch preview generation failed: {e}", exc_info=True)
203
+
204
+ return results
205
+
206
+
207
+ async def add_preview_urls_to_formats(
208
+ formats: list[Format],
209
+ creative_agent_client: ADCPClient,
210
+ use_batch: bool = True,
211
+ output_format: str = "url",
212
+ ) -> list[dict[str, Any]]:
213
+ """
214
+ Add preview URLs to each format by generating sample manifests.
215
+
216
+ Uses batch API for 5-10x better performance when previewing multiple formats.
217
+
218
+ Args:
219
+ formats: List of Format objects
220
+ creative_agent_client: Client for the creative agent
221
+ use_batch: If True, use batch API (default). Set False to use individual requests.
222
+ output_format: "url" for iframe URLs, "html" for direct embedding
223
+
224
+ Returns:
225
+ List of format dicts with added preview_data fields
226
+ """
227
+ if not formats:
228
+ return []
229
+
230
+ generator = PreviewURLGenerator(creative_agent_client)
231
+
232
+ # Prepare all requests
233
+ format_requests = []
234
+ for fmt in formats:
235
+ sample_manifest = _create_sample_manifest_for_format(fmt)
236
+ if sample_manifest:
237
+ format_requests.append((fmt, sample_manifest))
238
+
239
+ if not format_requests:
240
+ return [fmt.model_dump(exclude_none=True) for fmt in formats]
241
+
242
+ # Use batch API if requested and we have multiple formats
243
+ if use_batch and len(format_requests) > 1:
244
+ # Batch mode - much faster!
245
+ batch_requests = [(fmt.format_id, manifest) for fmt, manifest in format_requests]
246
+ preview_data_list = await generator.get_preview_data_batch(
247
+ batch_requests, output_format=output_format
248
+ )
249
+
250
+ # Merge preview data back with formats
251
+ result = []
252
+ preview_idx = 0
253
+ for fmt in formats:
254
+ format_dict = fmt.model_dump(exclude_none=True)
255
+ # Check if this format had a manifest
256
+ if preview_idx < len(format_requests) and format_requests[preview_idx][0] == fmt:
257
+ preview_data = preview_data_list[preview_idx]
258
+ if preview_data:
259
+ format_dict["preview_data"] = preview_data
260
+ preview_idx += 1
261
+ result.append(format_dict)
262
+ return result
263
+ else:
264
+ # Fallback to individual requests (for single format or when batch disabled)
265
+ import asyncio
266
+
267
+ async def process_format(fmt: Format) -> dict[str, Any]:
268
+ """Process a single format and add preview data."""
269
+ format_dict = fmt.model_dump(exclude_none=True)
270
+
271
+ try:
272
+ sample_manifest = _create_sample_manifest_for_format(fmt)
273
+ if sample_manifest:
274
+ preview_data = await generator.get_preview_data_for_manifest(
275
+ fmt.format_id, sample_manifest
276
+ )
277
+ if preview_data:
278
+ format_dict["preview_data"] = preview_data
279
+ except Exception as e:
280
+ logger.warning(f"Failed to add preview data for format {fmt.format_id}: {e}")
281
+
282
+ return format_dict
283
+
284
+ return await asyncio.gather(*[process_format(fmt) for fmt in formats])
285
+
286
+
287
+ async def add_preview_urls_to_products(
288
+ products: list[Product],
289
+ creative_agent_client: ADCPClient,
290
+ use_batch: bool = True,
291
+ output_format: str = "url",
292
+ ) -> list[dict[str, Any]]:
293
+ """
294
+ Add preview URLs to products for their supported formats.
295
+
296
+ Uses batch API for 5-10x better performance when previewing many product formats.
297
+
298
+ Args:
299
+ products: List of Product objects
300
+ creative_agent_client: Client for the creative agent
301
+ use_batch: If True, use batch API (default). Set False to use individual requests.
302
+ output_format: "url" for iframe URLs, "html" for direct embedding
303
+
304
+ Returns:
305
+ List of product dicts with added format_previews field
306
+ """
307
+ if not products:
308
+ return []
309
+
310
+ generator = PreviewURLGenerator(creative_agent_client)
311
+
312
+ # Collect all unique format_id + manifest combinations across all products
313
+ all_requests: list[tuple[Product, FormatId, CreativeManifest]] = []
314
+ for product in products:
315
+ for format_id in product.format_ids:
316
+ sample_manifest = _create_sample_manifest_for_format_id(format_id, product)
317
+ if sample_manifest:
318
+ all_requests.append((product, format_id, sample_manifest))
319
+
320
+ if not all_requests:
321
+ return [p.model_dump(exclude_none=True) for p in products]
322
+
323
+ # Use batch API if requested and we have multiple requests
324
+ if use_batch and len(all_requests) > 1:
325
+ # Batch mode - much faster!
326
+ batch_requests = [(format_id, manifest) for _, format_id, manifest in all_requests]
327
+ preview_data_list = await generator.get_preview_data_batch(
328
+ batch_requests, output_format=output_format
329
+ )
330
+
331
+ # Map results back to products
332
+ # Build a mapping from product_id -> format_id -> preview_data
333
+ product_previews: dict[str, dict[str, dict[str, Any]]] = {}
334
+ for (product, format_id, _), preview_data in zip(all_requests, preview_data_list):
335
+ if preview_data:
336
+ if product.product_id not in product_previews:
337
+ product_previews[product.product_id] = {}
338
+ product_previews[product.product_id][format_id.id] = preview_data
339
+
340
+ # Add preview data to products
341
+ result = []
342
+ for product in products:
343
+ product_dict = product.model_dump(exclude_none=True)
344
+ if product.product_id in product_previews:
345
+ product_dict["format_previews"] = product_previews[product.product_id]
346
+ result.append(product_dict)
347
+ return result
348
+ else:
349
+ # Fallback to individual requests (for single product/format or when batch disabled)
350
+ import asyncio
351
+
352
+ async def process_product(product: Product) -> dict[str, Any]:
353
+ """Process a single product and add preview data for all its formats."""
354
+ product_dict = product.model_dump(exclude_none=True)
355
+
356
+ async def process_format(format_id: FormatId) -> tuple[str, dict[str, Any] | None]:
357
+ """Process a single format for this product."""
358
+ try:
359
+ sample_manifest = _create_sample_manifest_for_format_id(format_id, product)
360
+ if sample_manifest:
361
+ preview_data = await generator.get_preview_data_for_manifest(
362
+ format_id, sample_manifest
363
+ )
364
+ return (format_id.id, preview_data)
365
+ except Exception as e:
366
+ logger.warning(
367
+ f"Failed to generate preview for product {product.product_id}, "
368
+ f"format {format_id}: {e}"
369
+ )
370
+ return (format_id.id, None)
371
+
372
+ format_tasks = [process_format(fid) for fid in product.format_ids]
373
+ format_results = await asyncio.gather(*format_tasks)
374
+ format_previews = {
375
+ fid: data for fid, data in format_results if data is not None
376
+ }
377
+
378
+ if format_previews:
379
+ product_dict["format_previews"] = format_previews
380
+
381
+ return product_dict
382
+
383
+ return await asyncio.gather(*[process_product(product) for product in products])
384
+
385
+
386
+ def _create_sample_manifest_for_format(fmt: Format) -> CreativeManifest | None:
387
+ """
388
+ Create a sample manifest for a format.
389
+
390
+ Args:
391
+ fmt: Format object
392
+
393
+ Returns:
394
+ Sample CreativeManifest, or None if unable to create one
395
+ """
396
+ from adcp.types.generated import CreativeManifest
397
+
398
+ if not fmt.assets_required:
399
+ return None
400
+
401
+ assets: dict[str, Any] = {}
402
+
403
+ for asset in fmt.assets_required:
404
+ if isinstance(asset, dict):
405
+ asset_id = asset.get("asset_id")
406
+ asset_type = asset.get("type")
407
+
408
+ if asset_id:
409
+ assets[asset_id] = _create_sample_asset(asset_type)
410
+
411
+ if not assets:
412
+ return None
413
+
414
+ return CreativeManifest(format_id=fmt.format_id, assets=assets, promoted_offering=None)
415
+
416
+
417
+ def _create_sample_manifest_for_format_id(
418
+ format_id: FormatId, product: Product
419
+ ) -> CreativeManifest | None:
420
+ """
421
+ Create a sample manifest for a format ID referenced by a product.
422
+
423
+ Args:
424
+ format_id: Format identifier
425
+ product: Product that references this format
426
+
427
+ Returns:
428
+ Sample CreativeManifest with placeholder assets
429
+ """
430
+ from adcp.types.generated import CreativeManifest
431
+
432
+ assets = {
433
+ "primary_asset": "https://example.com/sample-image.jpg",
434
+ "clickthrough_url": "https://example.com",
435
+ }
436
+
437
+ return CreativeManifest(format_id=format_id, promoted_offering=product.name, assets=assets)
438
+
439
+
440
+ def _create_sample_asset(asset_type: str | None) -> Any:
441
+ """
442
+ Create a sample asset value based on asset type.
443
+
444
+ Args:
445
+ asset_type: Type of asset (image, video, text, url, etc.)
446
+
447
+ Returns:
448
+ Sample asset value
449
+ """
450
+ if asset_type == "image":
451
+ return "https://via.placeholder.com/300x250.png"
452
+ elif asset_type == "video":
453
+ return "https://example.com/sample-video.mp4"
454
+ elif asset_type == "text":
455
+ return "Sample advertising text"
456
+ elif asset_type == "url":
457
+ return "https://example.com"
458
+ elif asset_type == "html":
459
+ return "<div>Sample HTML</div>"
460
+ else:
461
+ return "https://example.com/sample-asset"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 1.0.5
3
+ Version: 1.1.0
4
4
  Summary: Official Python client for the Ad Context Protocol (AdCP)
5
5
  Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
6
  License: Apache-2.0
@@ -64,8 +64,8 @@ pip install adcp
64
64
  ```python
65
65
  from adcp import ADCPMultiAgentClient, AgentConfig, GetProductsRequest
66
66
 
67
- # Configure agents and handlers
68
- client = ADCPMultiAgentClient(
67
+ # Configure agents and handlers (context manager ensures proper cleanup)
68
+ async with ADCPMultiAgentClient(
69
69
  agents=[
70
70
  AgentConfig(
71
71
  id="agent_x",
@@ -91,21 +91,21 @@ client = ADCPMultiAgentClient(
91
91
  if metadata.status == "completed" else None
92
92
  )
93
93
  }
94
- )
95
-
96
- # Execute operation - library handles operation IDs, webhook URLs, context management
97
- agent = client.agent("agent_x")
98
- request = GetProductsRequest(brief="Coffee brands")
99
- result = await agent.get_products(request)
94
+ ) as client:
95
+ # Execute operation - library handles operation IDs, webhook URLs, context management
96
+ agent = client.agent("agent_x")
97
+ request = GetProductsRequest(brief="Coffee brands")
98
+ result = await agent.get_products(request)
100
99
 
101
- # Check result
102
- if result.status == "completed":
103
- # Agent completed synchronously!
104
- print(f"✅ Sync completion: {len(result.data.products)} products")
100
+ # Check result
101
+ if result.status == "completed":
102
+ # Agent completed synchronously!
103
+ print(f"✅ Sync completion: {len(result.data.products)} products")
105
104
 
106
- if result.status == "submitted":
107
- # Agent will send webhook when complete
108
- print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
105
+ if result.status == "submitted":
106
+ # Agent will send webhook when complete
107
+ print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
108
+ # Connections automatically cleaned up here
109
109
  ```
110
110
 
111
111
  ## Features
@@ -210,6 +210,51 @@ Or use the CLI:
210
210
  uvx adcp --debug myagent get_products '{"brief":"TV ads"}'
211
211
  ```
212
212
 
213
+ ### Resource Management
214
+
215
+ **Why use async context managers?**
216
+ - Ensures HTTP connections are properly closed, preventing resource leaks
217
+ - Handles cleanup even when exceptions occur
218
+ - Required for production applications with connection pooling
219
+ - Prevents issues with async task group cleanup in MCP protocol
220
+
221
+ The recommended pattern uses async context managers:
222
+
223
+ ```python
224
+ from adcp import ADCPClient, AgentConfig, GetProductsRequest
225
+
226
+ # Recommended: Automatic cleanup with context manager
227
+ config = AgentConfig(id="agent_x", agent_uri="https://...", protocol="a2a")
228
+ async with ADCPClient(config) as client:
229
+ request = GetProductsRequest(brief="Coffee brands")
230
+ result = await client.get_products(request)
231
+ # Connection automatically closed on exit
232
+
233
+ # Multi-agent client also supports context managers
234
+ async with ADCPMultiAgentClient(agents) as client:
235
+ # Execute across all agents in parallel
236
+ results = await client.get_products(request)
237
+ # All agent connections closed automatically (even if some failed)
238
+ ```
239
+
240
+ Manual cleanup is available for special cases (e.g., managing client lifecycle manually):
241
+
242
+ ```python
243
+ # Use manual cleanup when you need fine-grained control over lifecycle
244
+ client = ADCPClient(config)
245
+ try:
246
+ result = await client.get_products(request)
247
+ finally:
248
+ await client.close() # Explicit cleanup
249
+ ```
250
+
251
+ **When to use manual cleanup:**
252
+ - Managing client lifecycle across multiple functions
253
+ - Testing scenarios requiring explicit control
254
+ - Integration with frameworks that manage resources differently
255
+
256
+ In most cases, prefer the context manager pattern.
257
+
213
258
  ### Error Handling
214
259
 
215
260
  The library provides a comprehensive exception hierarchy with helpful error messages:
@@ -0,0 +1,23 @@
1
+ adcp/__init__.py,sha256=p3emIEsC2FN_jcnHOP62BRPs6h4lgIhFselvAlTM0LI,2512
2
+ adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
3
+ adcp/client.py,sha256=xs_sG7soRH1szk0S0rFu_6Ge4Ffe2aUdaTYnLmvteeo,27950
4
+ adcp/config.py,sha256=Vsy7ZPOI8G3fB_i5Nk-CHbC7wdasCUWuKlos0fwA0kY,2017
5
+ adcp/exceptions.py,sha256=dNRMKV23DlkGKyB9Xmt6MtlhvDu1crjzD_en4nAEwDY,4399
6
+ adcp/protocols/__init__.py,sha256=6UFwACQ0QadBUzy17wUROHqsJDp8ztPW2jzyl53Zh_g,262
7
+ adcp/protocols/a2a.py,sha256=FHgc6G_eU2qD0vH7_RyS1eZvUFSb2j3-EsceoHPi384,12467
8
+ adcp/protocols/base.py,sha256=CGqUilQv_ymhnfdowBV_HJhIxYUDM3sRO7ahW-kRB0M,5087
9
+ adcp/protocols/mcp.py,sha256=eIk8snCinZm-ZjdarGVMt5nEYJ4_8POM9Fa5Mkw7xxU,15902
10
+ adcp/types/__init__.py,sha256=3E_TJUXqQQFcjmSZZSPLwqBP3s_ijsH2LDeuOU-MP30,402
11
+ adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
12
+ adcp/types/generated.py,sha256=j21CgpQExfd2gZTEnDUlVO3hvBmdn-4yBzgC86GUEnI,52485
13
+ adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
14
+ adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
15
+ adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
16
+ adcp/utils/preview_cache.py,sha256=8_2qs5CgrHv1_WOnD4bs43VWueu-rcZRu5PZMQ_lyuE,17573
17
+ adcp/utils/response_parser.py,sha256=NQTLlbvmnM_tE4B5w3oB1Wshny1p-Uh8IWbghlwoNJc,4057
18
+ adcp-1.1.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
19
+ adcp-1.1.0.dist-info/METADATA,sha256=51wZBtGtYyiqVRSaZtPW3OqvQOcZurM-A2FUvqRgrV8,14455
20
+ adcp-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ adcp-1.1.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
22
+ adcp-1.1.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
23
+ adcp-1.1.0.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- adcp/__init__.py,sha256=e7jeHMFSk_DxjaIWgRGqCR3A0fTOKtE-sMgulASLCWM,2512
2
- adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
3
- adcp/client.py,sha256=iIZUy5j25H48gGYlR_VuVsB9zP6SF1HtRssdPs2VJkc,24232
4
- adcp/config.py,sha256=Vsy7ZPOI8G3fB_i5Nk-CHbC7wdasCUWuKlos0fwA0kY,2017
5
- adcp/exceptions.py,sha256=dNRMKV23DlkGKyB9Xmt6MtlhvDu1crjzD_en4nAEwDY,4399
6
- adcp/protocols/__init__.py,sha256=6UFwACQ0QadBUzy17wUROHqsJDp8ztPW2jzyl53Zh_g,262
7
- adcp/protocols/a2a.py,sha256=TN26ac98h2NUZTTs39Tyd6pVoS3k-sASuLKhLpdYV-A,12255
8
- adcp/protocols/base.py,sha256=CGqUilQv_ymhnfdowBV_HJhIxYUDM3sRO7ahW-kRB0M,5087
9
- adcp/protocols/mcp.py,sha256=iQTr5QkZbUj42cc_ca7esvcV4EgcOpI8IT6akut9-UA,16028
10
- adcp/types/__init__.py,sha256=3E_TJUXqQQFcjmSZZSPLwqBP3s_ijsH2LDeuOU-MP30,402
11
- adcp/types/core.py,sha256=BO6188PI8lIiVjpiSR0pmsCuXq3Tg9KlkHC5N2fXFxw,4824
12
- adcp/types/generated.py,sha256=KoILEa5Gg0tsjMYoqDEWulKvPzS0333L3qj75AHr4yI,50855
13
- adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
14
- adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
15
- adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
16
- adcp/utils/response_parser.py,sha256=NQTLlbvmnM_tE4B5w3oB1Wshny1p-Uh8IWbghlwoNJc,4057
17
- adcp-1.0.5.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
18
- adcp-1.0.5.dist-info/METADATA,sha256=pmJWpMYNFVbyME-LpWm_Q9xFe3adBpivni17xNE2yaY,12724
19
- adcp-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- adcp-1.0.5.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
21
- adcp-1.0.5.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
22
- adcp-1.0.5.dist-info/RECORD,,
File without changes