adcp 1.4.1__py3-none-any.whl → 1.6.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
@@ -7,8 +7,21 @@ Official Python client for the Ad Context Protocol (AdCP).
7
7
  Supports both A2A and MCP protocols with full type safety.
8
8
  """
9
9
 
10
+ from adcp.adagents import (
11
+ domain_matches,
12
+ fetch_adagents,
13
+ get_all_properties,
14
+ get_all_tags,
15
+ get_properties_by_agent,
16
+ identifiers_match,
17
+ verify_agent_authorization,
18
+ verify_agent_for_property,
19
+ )
10
20
  from adcp.client import ADCPClient, ADCPMultiAgentClient
11
21
  from adcp.exceptions import (
22
+ AdagentsNotFoundError,
23
+ AdagentsTimeoutError,
24
+ AdagentsValidationError,
12
25
  ADCPAuthenticationError,
13
26
  ADCPConnectionError,
14
27
  ADCPError,
@@ -150,7 +163,7 @@ from adcp.types.generated import (
150
163
  TaskStatus as GeneratedTaskStatus,
151
164
  )
152
165
 
153
- __version__ = "1.4.1"
166
+ __version__ = "1.6.0"
154
167
 
155
168
  __all__ = [
156
169
  # Client classes
@@ -162,6 +175,15 @@ __all__ = [
162
175
  "TaskResult",
163
176
  "TaskStatus",
164
177
  "WebhookMetadata",
178
+ # Adagents validation
179
+ "fetch_adagents",
180
+ "verify_agent_authorization",
181
+ "verify_agent_for_property",
182
+ "domain_matches",
183
+ "identifiers_match",
184
+ "get_all_properties",
185
+ "get_all_tags",
186
+ "get_properties_by_agent",
165
187
  # Test helpers
166
188
  "test_agent",
167
189
  "test_agent_a2a",
@@ -185,6 +207,9 @@ __all__ = [
185
207
  "ADCPToolNotFoundError",
186
208
  "ADCPWebhookError",
187
209
  "ADCPWebhookSignatureError",
210
+ "AdagentsValidationError",
211
+ "AdagentsNotFoundError",
212
+ "AdagentsTimeoutError",
188
213
  # Request/Response types
189
214
  "ActivateSignalRequest",
190
215
  "ActivateSignalResponse",
adcp/adagents.py ADDED
@@ -0,0 +1,521 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Utilities for fetching, parsing, and validating adagents.json files per the AdCP specification.
5
+
6
+ Publishers declare authorized sales agents via adagents.json files hosted at
7
+ https://{publisher_domain}/.well-known/adagents.json. This module provides utilities
8
+ for sales agents to verify they are authorized for specific properties.
9
+ """
10
+
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ import httpx
15
+
16
+ from adcp.exceptions import AdagentsNotFoundError, AdagentsTimeoutError, AdagentsValidationError
17
+
18
+
19
+ def _normalize_domain(domain: str) -> str:
20
+ """Normalize domain for comparison - strip, lowercase, remove trailing dots/slashes.
21
+
22
+ Args:
23
+ domain: Domain to normalize
24
+
25
+ Returns:
26
+ Normalized domain string
27
+
28
+ Raises:
29
+ AdagentsValidationError: If domain contains invalid patterns
30
+ """
31
+ domain = domain.strip().lower()
32
+ # Remove both trailing slashes and dots iteratively
33
+ while domain.endswith("/") or domain.endswith("."):
34
+ domain = domain.rstrip("/").rstrip(".")
35
+
36
+ # Check for invalid patterns
37
+ if not domain or ".." in domain:
38
+ raise AdagentsValidationError(f"Invalid domain format: {domain!r}")
39
+
40
+ return domain
41
+
42
+
43
+ def _validate_publisher_domain(domain: str) -> str:
44
+ """Validate and sanitize publisher domain for security.
45
+
46
+ Args:
47
+ domain: Publisher domain to validate
48
+
49
+ Returns:
50
+ Validated and normalized domain
51
+
52
+ Raises:
53
+ AdagentsValidationError: If domain is invalid or contains suspicious characters
54
+ """
55
+ # Check for suspicious characters BEFORE stripping (to catch injection attempts)
56
+ suspicious_chars = ["\\", "@", "\n", "\r", "\t"]
57
+ for char in suspicious_chars:
58
+ if char in domain:
59
+ raise AdagentsValidationError(
60
+ f"Invalid character in publisher domain: {char!r}"
61
+ )
62
+
63
+ domain = domain.strip()
64
+
65
+ # Check basic constraints
66
+ if not domain:
67
+ raise AdagentsValidationError("Publisher domain cannot be empty")
68
+ if len(domain) > 253: # DNS maximum length
69
+ raise AdagentsValidationError(f"Publisher domain too long: {len(domain)} chars (max 253)")
70
+
71
+ # Check for spaces after stripping leading/trailing whitespace
72
+ if " " in domain:
73
+ raise AdagentsValidationError(
74
+ "Invalid character in publisher domain: ' '"
75
+ )
76
+
77
+ # Remove protocol if present (common user error) - do this BEFORE checking for slashes
78
+ if "://" in domain:
79
+ domain = domain.split("://", 1)[1]
80
+
81
+ # Remove path if present (should only be domain) - do this BEFORE checking for slashes
82
+ if "/" in domain:
83
+ domain = domain.split("/", 1)[0]
84
+
85
+ # Normalize
86
+ domain = _normalize_domain(domain)
87
+
88
+ # Final validation - must look like a domain
89
+ if "." not in domain:
90
+ raise AdagentsValidationError(
91
+ f"Publisher domain must contain at least one dot: {domain!r}"
92
+ )
93
+
94
+ return domain
95
+
96
+
97
+ def normalize_url(url: str) -> str:
98
+ """Normalize URL by removing protocol and trailing slash.
99
+
100
+ Args:
101
+ url: URL to normalize
102
+
103
+ Returns:
104
+ Normalized URL (domain/path without protocol or trailing slash)
105
+ """
106
+ parsed = urlparse(url)
107
+ normalized = parsed.netloc + parsed.path
108
+ return normalized.rstrip("/")
109
+
110
+
111
+ def domain_matches(property_domain: str, agent_domain_pattern: str) -> bool:
112
+ """Check if domains match per AdCP rules.
113
+
114
+ Rules:
115
+ - Exact match always succeeds
116
+ - 'example.com' matches www.example.com, m.example.com (common subdomains)
117
+ - 'subdomain.example.com' matches that specific subdomain only
118
+ - '*.example.com' matches all subdomains
119
+
120
+ Args:
121
+ property_domain: Domain from property
122
+ agent_domain_pattern: Domain pattern from adagents.json
123
+
124
+ Returns:
125
+ True if domains match per AdCP rules
126
+ """
127
+ # Normalize both domains for comparison
128
+ try:
129
+ property_domain = _normalize_domain(property_domain)
130
+ agent_domain_pattern = _normalize_domain(agent_domain_pattern)
131
+ except AdagentsValidationError:
132
+ # Invalid domain format - no match
133
+ return False
134
+
135
+ # Exact match
136
+ if property_domain == agent_domain_pattern:
137
+ return True
138
+
139
+ # Wildcard pattern (*.example.com)
140
+ if agent_domain_pattern.startswith("*."):
141
+ base_domain = agent_domain_pattern[2:]
142
+ return property_domain.endswith(f".{base_domain}")
143
+
144
+ # Bare domain matches common subdomains (www, m)
145
+ # If agent pattern is a bare domain (no subdomain), match www/m subdomains
146
+ if "." in agent_domain_pattern and not agent_domain_pattern.startswith("www."):
147
+ # Check if this looks like a bare domain (e.g., example.com)
148
+ parts = agent_domain_pattern.split(".")
149
+ if len(parts) == 2: # Looks like bare domain
150
+ common_subdomains = ["www", "m"]
151
+ for subdomain in common_subdomains:
152
+ if property_domain == f"{subdomain}.{agent_domain_pattern}":
153
+ return True
154
+
155
+ return False
156
+
157
+
158
+ def identifiers_match(
159
+ property_identifiers: list[dict[str, str]],
160
+ agent_identifiers: list[dict[str, str]],
161
+ ) -> bool:
162
+ """Check if any property identifier matches agent's authorized identifiers.
163
+
164
+ Args:
165
+ property_identifiers: Identifiers from property
166
+ (e.g., [{"type": "domain", "value": "cnn.com"}])
167
+ agent_identifiers: Identifiers from adagents.json
168
+
169
+ Returns:
170
+ True if any identifier matches
171
+
172
+ Notes:
173
+ - Domain identifiers use AdCP domain matching rules
174
+ - Other identifiers (bundle_id, roku_store_id, etc.) require exact match
175
+ """
176
+ for prop_id in property_identifiers:
177
+ prop_type = prop_id.get("type", "")
178
+ prop_value = prop_id.get("value", "")
179
+
180
+ for agent_id in agent_identifiers:
181
+ agent_type = agent_id.get("type", "")
182
+ agent_value = agent_id.get("value", "")
183
+
184
+ # Type must match
185
+ if prop_type != agent_type:
186
+ continue
187
+
188
+ # Domain identifiers use special matching rules
189
+ if prop_type == "domain":
190
+ if domain_matches(prop_value, agent_value):
191
+ return True
192
+ else:
193
+ # Other identifier types require exact match
194
+ if prop_value == agent_value:
195
+ return True
196
+
197
+ return False
198
+
199
+
200
+ def verify_agent_authorization(
201
+ adagents_data: dict[str, Any],
202
+ agent_url: str,
203
+ property_type: str | None = None,
204
+ property_identifiers: list[dict[str, str]] | None = None,
205
+ ) -> bool:
206
+ """Check if agent is authorized for a property.
207
+
208
+ Args:
209
+ adagents_data: Parsed adagents.json data
210
+ agent_url: URL of the sales agent to verify
211
+ property_type: Type of property (website, app, etc.) - optional
212
+ property_identifiers: List of identifiers to match - optional
213
+
214
+ Returns:
215
+ True if agent is authorized, False otherwise
216
+
217
+ Raises:
218
+ AdagentsValidationError: If adagents_data is malformed
219
+
220
+ Notes:
221
+ - If property_type/identifiers are None, checks if agent is authorized
222
+ for ANY property on this domain
223
+ - Implements AdCP domain matching rules
224
+ - Agent URLs are matched ignoring protocol and trailing slash
225
+ """
226
+ # Validate structure
227
+ if not isinstance(adagents_data, dict):
228
+ raise AdagentsValidationError("adagents_data must be a dictionary")
229
+
230
+ authorized_agents = adagents_data.get("authorized_agents")
231
+ if not isinstance(authorized_agents, list):
232
+ raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
233
+
234
+ # Normalize the agent URL for comparison
235
+ normalized_agent_url = normalize_url(agent_url)
236
+
237
+ # Check each authorized agent
238
+ for agent in authorized_agents:
239
+ if not isinstance(agent, dict):
240
+ continue
241
+
242
+ agent_url_from_json = agent.get("url", "")
243
+ if not agent_url_from_json:
244
+ continue
245
+
246
+ # Match agent URL (protocol-agnostic)
247
+ if normalize_url(agent_url_from_json) != normalized_agent_url:
248
+ continue
249
+
250
+ # Found matching agent - now check properties
251
+ properties = agent.get("properties")
252
+
253
+ # If properties field is missing or empty, agent is authorized for all properties
254
+ if properties is None or (isinstance(properties, list) and len(properties) == 0):
255
+ return True
256
+
257
+ # If no property filters specified, we found the agent - authorized
258
+ if property_type is None and property_identifiers is None:
259
+ return True
260
+
261
+ # Check specific property authorization
262
+ if isinstance(properties, list):
263
+ for prop in properties:
264
+ if not isinstance(prop, dict):
265
+ continue
266
+
267
+ # Check property type if specified
268
+ if property_type is not None:
269
+ prop_type = prop.get("property_type", "")
270
+ if prop_type != property_type:
271
+ continue
272
+
273
+ # Check identifiers if specified
274
+ if property_identifiers is not None:
275
+ prop_identifiers = prop.get("identifiers", [])
276
+ if not isinstance(prop_identifiers, list):
277
+ continue
278
+
279
+ if identifiers_match(property_identifiers, prop_identifiers):
280
+ return True
281
+ else:
282
+ # Property type matched and no identifier check needed
283
+ return True
284
+
285
+ return False
286
+
287
+
288
+ async def fetch_adagents(
289
+ publisher_domain: str,
290
+ timeout: float = 10.0,
291
+ user_agent: str = "AdCP-Client/1.0",
292
+ client: httpx.AsyncClient | None = None,
293
+ ) -> dict[str, Any]:
294
+ """Fetch and parse adagents.json from publisher domain.
295
+
296
+ Args:
297
+ publisher_domain: Domain hosting the adagents.json file
298
+ timeout: Request timeout in seconds
299
+ user_agent: User-Agent header for HTTP request
300
+ client: Optional httpx.AsyncClient for connection pooling.
301
+ If provided, caller is responsible for client lifecycle.
302
+ If None, a new client is created for this request.
303
+
304
+ Returns:
305
+ Parsed adagents.json data
306
+
307
+ Raises:
308
+ AdagentsNotFoundError: If adagents.json not found (404)
309
+ AdagentsValidationError: If JSON is invalid or malformed
310
+ AdagentsTimeoutError: If request times out
311
+
312
+ Notes:
313
+ For production use with multiple requests, pass a shared httpx.AsyncClient
314
+ to enable connection pooling and improve performance.
315
+ """
316
+ # Validate and normalize domain for security
317
+ publisher_domain = _validate_publisher_domain(publisher_domain)
318
+
319
+ # Construct URL
320
+ url = f"https://{publisher_domain}/.well-known/adagents.json"
321
+
322
+ try:
323
+ # Use provided client or create a new one
324
+ if client is not None:
325
+ # Reuse provided client (connection pooling)
326
+ response = await client.get(
327
+ url,
328
+ headers={"User-Agent": user_agent},
329
+ timeout=timeout,
330
+ follow_redirects=True,
331
+ )
332
+ else:
333
+ # Create new client for single request
334
+ async with httpx.AsyncClient() as new_client:
335
+ response = await new_client.get(
336
+ url,
337
+ headers={"User-Agent": user_agent},
338
+ timeout=timeout,
339
+ follow_redirects=True,
340
+ )
341
+
342
+ # Process response (same for both paths)
343
+ if response.status_code == 404:
344
+ raise AdagentsNotFoundError(publisher_domain)
345
+
346
+ if response.status_code != 200:
347
+ raise AdagentsValidationError(
348
+ f"Failed to fetch adagents.json: HTTP {response.status_code}"
349
+ )
350
+
351
+ # Parse JSON
352
+ try:
353
+ data = response.json()
354
+ except Exception as e:
355
+ raise AdagentsValidationError(f"Invalid JSON in adagents.json: {e}") from e
356
+
357
+ # Validate basic structure
358
+ if not isinstance(data, dict):
359
+ raise AdagentsValidationError("adagents.json must be a JSON object")
360
+
361
+ if "authorized_agents" not in data:
362
+ raise AdagentsValidationError(
363
+ "adagents.json must have 'authorized_agents' field"
364
+ )
365
+
366
+ if not isinstance(data["authorized_agents"], list):
367
+ raise AdagentsValidationError("'authorized_agents' must be an array")
368
+
369
+ return data
370
+
371
+ except httpx.TimeoutException as e:
372
+ raise AdagentsTimeoutError(publisher_domain, timeout) from e
373
+ except httpx.RequestError as e:
374
+ raise AdagentsValidationError(f"Failed to fetch adagents.json: {e}") from e
375
+
376
+
377
+ async def verify_agent_for_property(
378
+ publisher_domain: str,
379
+ agent_url: str,
380
+ property_identifiers: list[dict[str, str]],
381
+ property_type: str | None = None,
382
+ timeout: float = 10.0,
383
+ client: httpx.AsyncClient | None = None,
384
+ ) -> bool:
385
+ """Convenience wrapper to fetch adagents.json and verify authorization in one call.
386
+
387
+ Args:
388
+ publisher_domain: Domain hosting the adagents.json file
389
+ agent_url: URL of the sales agent to verify
390
+ property_identifiers: List of identifiers to match
391
+ property_type: Type of property (website, app, etc.) - optional
392
+ timeout: Request timeout in seconds
393
+ client: Optional httpx.AsyncClient for connection pooling
394
+
395
+ Returns:
396
+ True if agent is authorized, False otherwise
397
+
398
+ Raises:
399
+ AdagentsNotFoundError: If adagents.json not found (404)
400
+ AdagentsValidationError: If JSON is invalid or malformed
401
+ AdagentsTimeoutError: If request times out
402
+ """
403
+ adagents_data = await fetch_adagents(publisher_domain, timeout=timeout, client=client)
404
+ return verify_agent_authorization(
405
+ adagents_data=adagents_data,
406
+ agent_url=agent_url,
407
+ property_type=property_type,
408
+ property_identifiers=property_identifiers,
409
+ )
410
+
411
+
412
+ def get_all_properties(adagents_data: dict[str, Any]) -> list[dict[str, Any]]:
413
+ """Extract all properties from adagents.json data.
414
+
415
+ Args:
416
+ adagents_data: Parsed adagents.json data
417
+
418
+ Returns:
419
+ List of all properties across all authorized agents, with agent_url added
420
+
421
+ Raises:
422
+ AdagentsValidationError: If adagents_data is malformed
423
+ """
424
+ if not isinstance(adagents_data, dict):
425
+ raise AdagentsValidationError("adagents_data must be a dictionary")
426
+
427
+ authorized_agents = adagents_data.get("authorized_agents")
428
+ if not isinstance(authorized_agents, list):
429
+ raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
430
+
431
+ properties = []
432
+ for agent in authorized_agents:
433
+ if not isinstance(agent, dict):
434
+ continue
435
+
436
+ agent_url = agent.get("url", "")
437
+ if not agent_url:
438
+ continue
439
+
440
+ agent_properties = agent.get("properties", [])
441
+ if not isinstance(agent_properties, list):
442
+ continue
443
+
444
+ # Add each property with the agent URL for reference
445
+ for prop in agent_properties:
446
+ if isinstance(prop, dict):
447
+ # Create a copy and add agent_url
448
+ prop_with_agent = {**prop, "agent_url": agent_url}
449
+ properties.append(prop_with_agent)
450
+
451
+ return properties
452
+
453
+
454
+ def get_all_tags(adagents_data: dict[str, Any]) -> set[str]:
455
+ """Extract all unique tags from properties in adagents.json data.
456
+
457
+ Args:
458
+ adagents_data: Parsed adagents.json data
459
+
460
+ Returns:
461
+ Set of all unique tags across all properties
462
+
463
+ Raises:
464
+ AdagentsValidationError: If adagents_data is malformed
465
+ """
466
+ properties = get_all_properties(adagents_data)
467
+ tags = set()
468
+
469
+ for prop in properties:
470
+ prop_tags = prop.get("tags", [])
471
+ if isinstance(prop_tags, list):
472
+ for tag in prop_tags:
473
+ if isinstance(tag, str):
474
+ tags.add(tag)
475
+
476
+ return tags
477
+
478
+
479
+ def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> list[dict[str, Any]]:
480
+ """Get all properties authorized for a specific agent.
481
+
482
+ Args:
483
+ adagents_data: Parsed adagents.json data
484
+ agent_url: URL of the agent to filter by
485
+
486
+ Returns:
487
+ List of properties for the specified agent (empty if agent not found or no properties)
488
+
489
+ Raises:
490
+ AdagentsValidationError: If adagents_data is malformed
491
+ """
492
+ if not isinstance(adagents_data, dict):
493
+ raise AdagentsValidationError("adagents_data must be a dictionary")
494
+
495
+ authorized_agents = adagents_data.get("authorized_agents")
496
+ if not isinstance(authorized_agents, list):
497
+ raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
498
+
499
+ # Normalize the agent URL for comparison
500
+ normalized_agent_url = normalize_url(agent_url)
501
+
502
+ for agent in authorized_agents:
503
+ if not isinstance(agent, dict):
504
+ continue
505
+
506
+ agent_url_from_json = agent.get("url", "")
507
+ if not agent_url_from_json:
508
+ continue
509
+
510
+ # Match agent URL (protocol-agnostic)
511
+ if normalize_url(agent_url_from_json) != normalized_agent_url:
512
+ continue
513
+
514
+ # Found the agent - return their properties
515
+ properties = agent.get("properties", [])
516
+ if not isinstance(properties, list):
517
+ return []
518
+
519
+ return [p for p in properties if isinstance(p, dict)]
520
+
521
+ return []
adcp/exceptions.py CHANGED
@@ -153,3 +153,33 @@ class ADCPSimpleAPIError(ADCPError):
153
153
  f" # Handle error with full TaskResult context"
154
154
  )
155
155
  super().__init__(message, agent_id, None, suggestion)
156
+
157
+
158
+ class AdagentsValidationError(ADCPError):
159
+ """Base error for adagents.json validation issues."""
160
+
161
+
162
+ class AdagentsNotFoundError(AdagentsValidationError):
163
+ """adagents.json file not found (404)."""
164
+
165
+ def __init__(self, publisher_domain: str):
166
+ """Initialize not found error."""
167
+ message = f"adagents.json not found for domain: {publisher_domain}"
168
+ suggestion = (
169
+ "Verify that the publisher has deployed adagents.json to:\n"
170
+ f" https://{publisher_domain}/.well-known/adagents.json"
171
+ )
172
+ super().__init__(message, None, None, suggestion)
173
+
174
+
175
+ class AdagentsTimeoutError(AdagentsValidationError):
176
+ """Request for adagents.json timed out."""
177
+
178
+ def __init__(self, publisher_domain: str, timeout: float):
179
+ """Initialize timeout error."""
180
+ message = f"Request to fetch adagents.json timed out after {timeout}s"
181
+ suggestion = (
182
+ "The publisher's server may be slow or unresponsive.\n"
183
+ " Try increasing the timeout value or check the domain is correct."
184
+ )
185
+ super().__init__(message, None, None, suggestion)
adcp/types/__init__.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  """Type definitions for AdCP client."""
4
4
 
5
+ from adcp.types.base import AdCPBaseModel
5
6
  from adcp.types.core import (
6
7
  Activity,
7
8
  ActivityType,
@@ -14,6 +15,7 @@ from adcp.types.core import (
14
15
  )
15
16
 
16
17
  __all__ = [
18
+ "AdCPBaseModel",
17
19
  "AgentConfig",
18
20
  "Protocol",
19
21
  "TaskResult",
adcp/types/base.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ """Base model for AdCP types with spec-compliant serialization."""
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class AdCPBaseModel(BaseModel):
11
+ """Base model for AdCP types with spec-compliant serialization.
12
+
13
+ AdCP JSON schemas use additionalProperties: false and do not allow null
14
+ for optional fields. Therefore, optional fields must be omitted entirely
15
+ when not present (not sent as null).
16
+ """
17
+
18
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
19
+ if "exclude_none" not in kwargs:
20
+ kwargs["exclude_none"] = True
21
+ return super().model_dump(**kwargs)
22
+
23
+ def model_dump_json(self, **kwargs: Any) -> str:
24
+ if "exclude_none" not in kwargs:
25
+ kwargs["exclude_none"] = True
26
+ return super().model_dump_json(**kwargs)
adcp/types/generated.py CHANGED
@@ -14,7 +14,9 @@ from __future__ import annotations
14
14
  import re
15
15
  from typing import Any, Literal
16
16
 
17
- from pydantic import BaseModel, ConfigDict, Field, field_validator
17
+ from pydantic import ConfigDict, Field, field_validator
18
+
19
+ from adcp.types.base import AdCPBaseModel as BaseModel
18
20
 
19
21
 
20
22
 
@@ -158,6 +160,8 @@ class Targeting(BaseModel):
158
160
  geo_region_any_of: list[str] | None = Field(None, description="Restrict delivery to specific regions/states. Use for regulatory compliance or RCT testing.")
159
161
  geo_metro_any_of: list[str] | None = Field(None, description="Restrict delivery to specific metro areas (DMA codes). Use for regulatory compliance or RCT testing.")
160
162
  geo_postal_code_any_of: list[str] | None = Field(None, description="Restrict delivery to specific postal/ZIP codes. Use for regulatory compliance or RCT testing.")
163
+ axe_include_segment: str | None = Field(None, description="AXE segment ID to include for targeting")
164
+ axe_exclude_segment: str | None = Field(None, description="AXE segment ID to exclude from targeting")
161
165
  frequency_cap: FrequencyCap | None = None
162
166
 
163
167
 
@@ -635,6 +639,7 @@ class ActivateSignalRequest(BaseModel):
635
639
 
636
640
  signal_agent_segment_id: str = Field(description="The universal identifier for the signal to activate")
637
641
  destinations: list[Destination] = Field(description="Target destination(s) for activation. If the authenticated caller matches one of these destinations, activation keys will be included in the response.")
642
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
638
643
 
639
644
 
640
645
  class BuildCreativeRequest(BaseModel):
@@ -643,6 +648,7 @@ class BuildCreativeRequest(BaseModel):
643
648
  message: str | None = Field(None, description="Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative.")
644
649
  creative_manifest: CreativeManifest | None = Field(None, description="Creative manifest to transform or generate from. For pure generation, this should include the target format_id and any required input assets (e.g., promoted_offerings for generative formats). For transformation (e.g., resizing, reformatting), this is the complete creative to adapt.")
645
650
  target_format_id: FormatId = Field(description="Format ID to generate. The format definition specifies required input assets and output structure.")
651
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
646
652
 
647
653
 
648
654
  class CreateMediaBuyRequest(BaseModel):
@@ -655,6 +661,7 @@ class CreateMediaBuyRequest(BaseModel):
655
661
  start_time: StartTiming
656
662
  end_time: str = Field(description="Campaign end date/time in ISO 8601 format")
657
663
  reporting_webhook: Any | None = None
664
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
658
665
 
659
666
 
660
667
  class GetMediaBuyDeliveryRequest(BaseModel):
@@ -665,6 +672,7 @@ class GetMediaBuyDeliveryRequest(BaseModel):
665
672
  status_filter: Any | None = Field(None, description="Filter by status. Can be a single status or array of statuses")
666
673
  start_date: str | None = Field(None, description="Start date for reporting period (YYYY-MM-DD)")
667
674
  end_date: str | None = Field(None, description="End date for reporting period (YYYY-MM-DD)")
675
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
668
676
 
669
677
 
670
678
  class GetProductsRequest(BaseModel):
@@ -673,6 +681,7 @@ class GetProductsRequest(BaseModel):
673
681
  brief: str | None = Field(None, description="Natural language description of campaign requirements")
674
682
  brand_manifest: BrandManifestRef | None = Field(None, description="Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.")
675
683
  filters: dict[str, Any] | None = Field(None, description="Structured filters for product discovery")
684
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
676
685
 
677
686
 
678
687
  class GetSignalsRequest(BaseModel):
@@ -682,12 +691,14 @@ class GetSignalsRequest(BaseModel):
682
691
  deliver_to: dict[str, Any] = Field(description="Destination platforms where signals need to be activated")
683
692
  filters: dict[str, Any] | None = Field(None, description="Filters to refine results")
684
693
  max_results: int | None = Field(None, description="Maximum number of results to return")
694
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
685
695
 
686
696
 
687
697
  class ListAuthorizedPropertiesRequest(BaseModel):
688
698
  """Request parameters for discovering which publishers this agent is authorized to represent"""
689
699
 
690
700
  publisher_domains: list[str] | None = Field(None, description="Filter to specific publisher domains (optional). If omitted, returns all publishers this agent represents.")
701
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
691
702
 
692
703
 
693
704
  class ListCreativeFormatsRequest(BaseModel):
@@ -702,6 +713,7 @@ class ListCreativeFormatsRequest(BaseModel):
702
713
  min_height: int | None = Field(None, description="Minimum height in pixels (inclusive). Returns formats with height >= this value.")
703
714
  is_responsive: bool | None = Field(None, description="Filter for responsive formats that adapt to container size. When true, returns formats without fixed dimensions.")
704
715
  name_search: str | None = Field(None, description="Search for formats by name (case-insensitive partial match)")
716
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
705
717
 
706
718
 
707
719
  class ListCreativesRequest(BaseModel):
@@ -714,6 +726,7 @@ class ListCreativesRequest(BaseModel):
714
726
  include_performance: bool | None = Field(None, description="Include aggregated performance metrics in response")
715
727
  include_sub_assets: bool | None = Field(None, description="Include sub-assets (for carousel/native formats) in response")
716
728
  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)")
729
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
717
730
 
718
731
 
719
732
  class PackageRequest(BaseModel):
@@ -741,6 +754,7 @@ class ProvidePerformanceFeedbackRequest(BaseModel):
741
754
  creative_id: str | None = Field(None, description="Specific creative asset (if feedback is creative-specific)")
742
755
  metric_type: Literal["overall_performance", "conversion_rate", "brand_lift", "click_through_rate", "completion_rate", "viewability", "brand_safety", "cost_efficiency"] | None = Field(None, description="The business metric being measured")
743
756
  feedback_source: Literal["buyer_attribution", "third_party_measurement", "platform_analytics", "verification_partner"] | None = Field(None, description="Source of the performance data")
757
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
744
758
 
745
759
 
746
760
  class SyncCreativesRequest(BaseModel):
@@ -753,6 +767,7 @@ class SyncCreativesRequest(BaseModel):
753
767
  dry_run: bool | None = Field(None, description="When true, preview changes without applying them. Returns what would be created/updated/deleted.")
754
768
  validation_mode: Literal["strict", "lenient"] | None = Field(None, description="Validation strictness. 'strict' fails entire sync on any validation error. 'lenient' processes valid creatives and reports errors.")
755
769
  push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async sync notifications. Publisher will send webhook when sync completes if operation takes longer than immediate response time (typically for large bulk operations or manual approval/HITL).")
770
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
756
771
 
757
772
 
758
773
  class TasksGetRequest(BaseModel):
@@ -760,6 +775,7 @@ class TasksGetRequest(BaseModel):
760
775
 
761
776
  task_id: str = Field(description="Unique identifier of the task to retrieve")
762
777
  include_history: bool | None = Field(None, description="Include full conversation history for this task (may increase response size)")
778
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
763
779
 
764
780
 
765
781
  class TasksListRequest(BaseModel):
@@ -769,6 +785,7 @@ class TasksListRequest(BaseModel):
769
785
  sort: dict[str, Any] | None = Field(None, description="Sorting parameters")
770
786
  pagination: dict[str, Any] | None = Field(None, description="Pagination parameters")
771
787
  include_history: bool | None = Field(None, description="Include full conversation history for each task (may significantly increase response size)")
788
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
772
789
 
773
790
 
774
791
  class UpdateMediaBuyRequest(BaseModel):
@@ -781,6 +798,7 @@ class UpdateMediaBuyRequest(BaseModel):
781
798
  end_time: str | None = Field(None, description="New end date/time in ISO 8601 format")
782
799
  packages: list[dict[str, Any]] | None = Field(None, description="Package-specific updates")
783
800
  push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.")
801
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
784
802
 
785
803
 
786
804
  # Response containing the transformed or generated creative manifest, ready for use with preview_creative or sync_creatives. Returns either the complete creative manifest OR error information, never both.
@@ -791,6 +809,7 @@ class BuildCreativeResponseVariant1(BaseModel):
791
809
  model_config = ConfigDict(extra="forbid")
792
810
 
793
811
  creative_manifest: CreativeManifest = Field(description="The generated or transformed creative manifest")
812
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
794
813
 
795
814
 
796
815
  class BuildCreativeResponseVariant2(BaseModel):
@@ -799,6 +818,7 @@ class BuildCreativeResponseVariant2(BaseModel):
799
818
  model_config = ConfigDict(extra="forbid")
800
819
 
801
820
  errors: list[Error] = Field(description="Array of errors explaining why creative generation failed")
821
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
802
822
 
803
823
 
804
824
  # Union type for Build Creative Response
@@ -818,6 +838,7 @@ class GetMediaBuyDeliveryResponse(BaseModel):
818
838
  aggregated_totals: dict[str, Any] | None = Field(None, description="Combined metrics across all returned media buys. Only included in API responses (get_media_buy_delivery), not in webhook notifications.")
819
839
  media_buy_deliveries: list[dict[str, Any]] = Field(description="Array of delivery data for media buys. When used in webhook notifications, may contain multiple media buys aggregated by publisher. When used in get_media_buy_delivery API responses, typically contains requested media buys.")
820
840
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., missing delivery data, reporting platform issues)")
841
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
821
842
 
822
843
 
823
844
  class GetProductsResponse(BaseModel):
@@ -825,6 +846,7 @@ class GetProductsResponse(BaseModel):
825
846
 
826
847
  products: list[Product] = Field(description="Array of matching products")
827
848
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., product filtering issues)")
849
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
828
850
 
829
851
 
830
852
  class GetSignalsResponse(BaseModel):
@@ -832,6 +854,7 @@ class GetSignalsResponse(BaseModel):
832
854
 
833
855
  signals: list[dict[str, Any]] = Field(description="Array of matching signals")
834
856
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., signal discovery or pricing issues)")
857
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
835
858
 
836
859
 
837
860
  class ListAuthorizedPropertiesResponse(BaseModel):
@@ -844,6 +867,7 @@ class ListAuthorizedPropertiesResponse(BaseModel):
844
867
  advertising_policies: str | None = Field(None, description="Publisher's advertising content policies, restrictions, and guidelines in natural language. May include prohibited categories, blocked advertisers, restricted tactics, brand safety requirements, or links to full policy documentation.")
845
868
  last_updated: str | None = Field(None, description="ISO 8601 timestamp of when the agent's publisher authorization list was last updated. Buyers can use this to determine if their cached publisher adagents.json files might be stale.")
846
869
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., property availability issues)")
870
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
847
871
 
848
872
 
849
873
  class ListCreativeFormatsResponse(BaseModel):
@@ -852,6 +876,7 @@ class ListCreativeFormatsResponse(BaseModel):
852
876
  formats: list[Format] = Field(description="Full format definitions for all formats this agent supports. Each format's authoritative source is indicated by its agent_url field.")
853
877
  creative_agents: list[dict[str, Any]] | None = Field(None, description="Optional: Creative agents that provide additional formats. Buyers can recursively query these agents to discover more formats. No authentication required for list_creative_formats.")
854
878
  errors: list[Error] | None = Field(None, description="Task-specific errors and warnings")
879
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
855
880
 
856
881
 
857
882
  class ListCreativesResponse(BaseModel):
@@ -862,6 +887,7 @@ class ListCreativesResponse(BaseModel):
862
887
  creatives: list[dict[str, Any]] = Field(description="Array of creative assets matching the query")
863
888
  format_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by format type")
864
889
  status_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by status")
890
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
865
891
 
866
892
 
867
893
  # Response payload for provide_performance_feedback task. Returns either success confirmation OR error information, never both.
@@ -872,6 +898,7 @@ class ProvidePerformanceFeedbackResponseVariant1(BaseModel):
872
898
  model_config = ConfigDict(extra="forbid")
873
899
 
874
900
  success: Literal[True] = Field(description="Whether the performance feedback was successfully received")
901
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
875
902
 
876
903
 
877
904
  class ProvidePerformanceFeedbackResponseVariant2(BaseModel):
@@ -880,6 +907,7 @@ class ProvidePerformanceFeedbackResponseVariant2(BaseModel):
880
907
  model_config = ConfigDict(extra="forbid")
881
908
 
882
909
  errors: list[Error] = Field(description="Array of errors explaining why feedback was rejected (e.g., invalid measurement period, missing campaign data)")
910
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
883
911
 
884
912
 
885
913
  # Union type for Provide Performance Feedback Response
@@ -900,6 +928,7 @@ class TasksGetResponse(BaseModel):
900
928
  progress: dict[str, Any] | None = Field(None, description="Progress information for long-running tasks")
901
929
  error: dict[str, Any] | None = Field(None, description="Error details for failed tasks")
902
930
  history: list[dict[str, Any]] | None = Field(None, description="Complete conversation history for this task (only included if include_history was true in request)")
931
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
903
932
 
904
933
 
905
934
  class TasksListResponse(BaseModel):
@@ -908,6 +937,7 @@ class TasksListResponse(BaseModel):
908
937
  query_summary: dict[str, Any] = Field(description="Summary of the query that was executed")
909
938
  tasks: list[dict[str, Any]] = Field(description="Array of tasks matching the query criteria")
910
939
  pagination: dict[str, Any] = Field(description="Pagination information")
940
+ context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
911
941
 
912
942
 
913
943
 
@@ -917,6 +947,7 @@ class TasksListResponse(BaseModel):
917
947
  # The simple code generator produces type aliases (e.g., PreviewCreativeRequest = Any)
918
948
  # for complex schemas that use oneOf. We override them here with proper Pydantic classes
919
949
  # to maintain type safety and enable batch API support.
950
+ # Note: All classes inherit from BaseModel (which is aliased to AdCPBaseModel for exclude_none).
920
951
 
921
952
 
922
953
  class FormatId(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 1.4.1
3
+ Version: 1.6.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
@@ -461,6 +461,50 @@ auth = index.get_agent_authorizations("https://agent-x.com")
461
461
  premium = index.find_agents_by_property_tags(["premium", "ctv"])
462
462
  ```
463
463
 
464
+ ## Publisher Authorization Validation
465
+
466
+ Verify sales agents are authorized to sell publisher properties via adagents.json:
467
+
468
+ ```python
469
+ from adcp import (
470
+ fetch_adagents,
471
+ verify_agent_authorization,
472
+ verify_agent_for_property,
473
+ )
474
+
475
+ # Fetch and parse adagents.json from publisher
476
+ adagents_data = await fetch_adagents("publisher.com")
477
+
478
+ # Verify agent authorization for a property
479
+ is_authorized = verify_agent_authorization(
480
+ adagents_data=adagents_data,
481
+ agent_url="https://sales-agent.example.com",
482
+ property_type="website",
483
+ property_identifiers=[{"type": "domain", "value": "publisher.com"}]
484
+ )
485
+
486
+ # Or use convenience wrapper (fetch + verify in one call)
487
+ is_authorized = await verify_agent_for_property(
488
+ publisher_domain="publisher.com",
489
+ agent_url="https://sales-agent.example.com",
490
+ property_identifiers=[{"type": "domain", "value": "publisher.com"}],
491
+ property_type="website"
492
+ )
493
+ ```
494
+
495
+ **Domain Matching Rules:**
496
+ - Exact match: `example.com` matches `example.com`
497
+ - Common subdomains: `www.example.com` matches `example.com`
498
+ - Wildcards: `api.example.com` matches `*.example.com`
499
+ - Protocol-agnostic: `http://agent.com` matches `https://agent.com`
500
+
501
+ **Use Cases:**
502
+ - Sales agents verify authorization before accepting media buys
503
+ - Publishers test their adagents.json files
504
+ - Developer tools build authorization validators
505
+
506
+ See `examples/adagents_validation.py` for complete examples.
507
+
464
508
  ## CLI Tool
465
509
 
466
510
  The `adcp` command-line tool provides easy interaction with AdCP agents without writing code.
@@ -1,8 +1,9 @@
1
- adcp/__init__.py,sha256=zV1J1z4uR1C7rGIccifeDGjN8pnZI1o280FioRvwRLM,6976
1
+ adcp/__init__.py,sha256=AssQu9RgVTit-LqDxtQ-pgKuxjoDQr67HhiIxKMmSFA,7612
2
2
  adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
3
+ adcp/adagents.py,sha256=thq6qZScRHLWrRR4DjGlZzHpC1ZssQqhc5l0pbKSa-Q,17483
3
4
  adcp/client.py,sha256=4qoFNDT5swzi4w5bnWJ5nVTG5JIL0xnLJOJMGeMyci4,28412
4
5
  adcp/config.py,sha256=Vsy7ZPOI8G3fB_i5Nk-CHbC7wdasCUWuKlos0fwA0kY,2017
5
- adcp/exceptions.py,sha256=9L7a3TKrMhmbEqDM0Qjdu3dQAFeKCSW4nc1IcLGSj4Y,5597
6
+ adcp/exceptions.py,sha256=1aZEWpaM92OxD2jl9yKsqJp5ReSWaj0S0DFhxChhLlA,6732
6
7
  adcp/simple.py,sha256=FgPYWT32BNXkQz07r2x2gXgOmOikWLi88SzN5UIVSiU,10440
7
8
  adcp/protocols/__init__.py,sha256=6UFwACQ0QadBUzy17wUROHqsJDp8ztPW2jzyl53Zh_g,262
8
9
  adcp/protocols/a2a.py,sha256=FHgc6G_eU2qD0vH7_RyS1eZvUFSb2j3-EsceoHPi384,12467
@@ -10,17 +11,18 @@ adcp/protocols/base.py,sha256=vBHD23Fzl_CCk_Gy9nvSbBYopcJlYkYyzoz-rhI8wHg,5214
10
11
  adcp/protocols/mcp.py,sha256=d9uSpGd0BKvQ0JxztkfDvHwoDrDYhuiw5oivpYOAbmM,16647
11
12
  adcp/testing/__init__.py,sha256=ZWp_floWjVZfy8RBG5v_FUXQ8YbN7xjXvVcX-_zl_HU,1416
12
13
  adcp/testing/test_helpers.py,sha256=4n8fZYy1cVpjZpFW2SxBzpC8fmY-MBFrzY4tIPqe4rQ,10028
13
- adcp/types/__init__.py,sha256=3E_TJUXqQQFcjmSZZSPLwqBP3s_ijsH2LDeuOU-MP30,402
14
+ adcp/types/__init__.py,sha256=FXm4210pkzOIQQEgpe-EeLLd7mxofzEgKLGl1r8fj4o,465
15
+ adcp/types/base.py,sha256=QoEuVfI4yzefup0dc2KN11AcJTbcGxRep7xOw5hXfs8,837
14
16
  adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
15
- adcp/types/generated.py,sha256=Ig4ucbJzKRuHlwYzsqvMF9M3w2KghhQQqsXuOnBqVMM,74993
17
+ adcp/types/generated.py,sha256=NY6A6eBw8wscSqJlLhW4OFvhTaZegcNR1hQ5VU51IFM,81526
16
18
  adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
17
19
  adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
18
20
  adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
19
21
  adcp/utils/preview_cache.py,sha256=8_2qs5CgrHv1_WOnD4bs43VWueu-rcZRu5PZMQ_lyuE,17573
20
22
  adcp/utils/response_parser.py,sha256=uPk2vIH-RYZmq7y3i8lC4HTMQ3FfKdlgXKTjgJ1955M,6253
21
- adcp-1.4.1.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
22
- adcp-1.4.1.dist-info/METADATA,sha256=Y2iI6jHEWqkxqXSHHv5m2NX2OHet5KyEIPktdlNfOJo,19931
23
- adcp-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- adcp-1.4.1.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
25
- adcp-1.4.1.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
26
- adcp-1.4.1.dist-info/RECORD,,
23
+ adcp-1.6.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
24
+ adcp-1.6.0.dist-info/METADATA,sha256=9p6HjEycLX_f45n5H1l_gsDlLU67EpDJM3jBWZhLcqk,21345
25
+ adcp-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ adcp-1.6.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
27
+ adcp-1.6.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
28
+ adcp-1.6.0.dist-info/RECORD,,
File without changes