Skip to content

Commit 16e6bbb

Browse files
authored
fix(sdk): address beta testing and migration gaps (#1056)
* fix(sdk): address beta testing and migration gaps * test(testing): make server patches Python 3.10 compatible
1 parent aedf2cd commit 16e6bbb

11 files changed

Lines changed: 425 additions & 31 deletions

src/adcp/migrate/v3_to_v4.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from __future__ import annotations
5050

5151
import argparse
52+
import importlib
5253
import json
5354
import re
5455
import sys
@@ -170,8 +171,16 @@
170171
# module. Numbered codegen names cannot be mapped safely by symbol alone: the
171172
# same bare name may describe a different schema in another generated module.
172173
GENERATED_POC_SOURCE_SYMBOL_MAP: dict[tuple[str, str], str] = {
174+
("core.account", "GovernanceAgent"): "adcp.types.CoreGovernanceAgent",
173175
("core.account_ref", "AccountReference1"): "adcp.types.AccountIdReference",
174176
("core.account_ref", "AccountReference2"): "adcp.types.InlineAccountReference",
177+
("core.creative_asset", "CreativeAsset"): "adcp.types.LegacyCreativeAsset",
178+
("core.creative_filters", "CreativeFilters"): "adcp.types.LegacyCreativeFilters",
179+
("core.product_filters", "Country"): "adcp.types.ProductFilterCountry",
180+
("core.product_format_declaration", "ProductFormatDeclaration"): (
181+
"adcp.types.LegacyProductFormatDeclaration"
182+
),
183+
("core.property", "Identifier"): "adcp.types.PropertyIdentifier",
175184
("core.vendor_pricing_option", "VendorPricingOption"): ("adcp.types.VendorPricingOptionUnion"),
176185
("core.vendor_pricing_option", "VendorPricingOption1"): ("adcp.types.CpmVendorPricingOption"),
177186
("core.vendor_pricing_option", "VendorPricingOption2"): (
@@ -196,6 +205,11 @@
196205
("media_buy.update_media_buy_response", "UpdateMediaBuyResponse2"): (
197206
"adcp.types.LegacyUpdateMediaBuyErrorResponse"
198207
),
208+
("media_buy.update_media_buy_response", "UpdateMediaBuyResponse1"): (
209+
"adcp.types.LegacyUpdateMediaBuySuccessResponse"
210+
),
211+
("creative.list_creatives_request", "Sort"): "adcp.types.ListCreativesSort",
212+
("signals.get_signals_response", "Signal"): "adcp.types.GetSignalsSignal",
199213
}
200214

201215

@@ -210,12 +224,38 @@
210224
)
211225

212226

213-
def _generated_symbol_replacement(module: str, symbol: str) -> str | None:
227+
def _proposed_generated_symbol_replacement(module: str, symbol: str) -> str | None:
214228
return GENERATED_POC_SOURCE_SYMBOL_MAP.get((module, symbol)) or GENERATED_POC_SYMBOL_MAP.get(
215229
symbol
216230
)
217231

218232

233+
def _replacement_is_identical(module: str, symbol: str, replacement: str) -> bool:
234+
"""Verify that a private class and its proposed public target are identical."""
235+
if not module or not replacement.startswith("adcp.types."):
236+
return False
237+
try:
238+
source_module = importlib.import_module(f"adcp.types.generated_poc.{module}")
239+
public_module = importlib.import_module("adcp.types")
240+
source = getattr(source_module, symbol)
241+
public = getattr(public_module, replacement.removeprefix("adcp.types."))
242+
except (AttributeError, ImportError):
243+
return False
244+
return source is public
245+
246+
247+
def _generated_symbol_replacement(module: str, symbol: str) -> str | None:
248+
replacement = _proposed_generated_symbol_replacement(module, symbol)
249+
if replacement is None or not _replacement_is_identical(module, symbol, replacement):
250+
return None
251+
return replacement
252+
253+
254+
def _unsafe_replacement_hint(module: str, symbol: str, replacement: str) -> str:
255+
source = f"adcp.types.generated_poc.{module}.{symbol}"
256+
return f"SKIP: source {source} is not identical to {replacement} — " "manual rewrite required"
257+
258+
219259
# Regex for numbered Assets direct imports (``Assets5``, ``Assets14``, etc).
220260
# Bare ``Assets`` (no digits) is a legitimate base class alias; the
221261
# regex requires at least one digit to avoid false positives.
@@ -575,6 +615,19 @@ def scan_file(
575615
# the import-path fix covers it.
576616
if auto_apply and symbol in NUMBERED_ASSETS_RENAMES:
577617
continue
618+
proposed = _proposed_generated_symbol_replacement(module, symbol)
619+
if proposed is not None:
620+
findings.append(
621+
Finding(
622+
kind="flag_private",
623+
path=str(path),
624+
line=lineno,
625+
column=sym_col,
626+
before=symbol,
627+
hint=_unsafe_replacement_hint(module, symbol, proposed),
628+
)
629+
)
630+
continue
578631
findings.append(
579632
Finding(
580633
kind="flag_private",

src/adcp/testing/decisioning.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from __future__ import annotations
2727

28+
import warnings
2829
from contextlib import asynccontextmanager
2930
from typing import TYPE_CHECKING, Any, Literal
3031
from urllib.parse import urlparse
@@ -49,7 +50,12 @@
4950
)
5051
from adcp.server.auth import BearerTokenAuth
5152
from adcp.server.helpers import ResponseEnhancer
52-
from adcp.server.serve import ASGIMiddlewareEntry, ContextFactory, SkillMiddleware
53+
from adcp.server.serve import (
54+
ASGIMiddlewareEntry,
55+
ContextFactory,
56+
LifespanHook,
57+
SkillMiddleware,
58+
)
5359
from adcp.server.spec_compat import PreValidationHooks
5460

5561

@@ -148,12 +154,17 @@ def build_asgi_app(
148154
context_factory: ContextFactory | None = None,
149155
middleware: Sequence[SkillMiddleware] | None = None,
150156
streaming_responses: bool = False,
157+
stateless_http: bool = False,
158+
session_idle_timeout: float | None = 1800.0,
159+
max_active_sessions: int | None = None,
151160
enable_dns_rebinding_protection: bool | None = None,
152161
max_request_size: int | None = None,
153162
validation: ValidationHookConfig | None = DEFAULT_VALIDATION,
154163
discovery_base_url: str | None = None,
155164
pre_validation_hooks: PreValidationHooks | None = None,
156165
response_enhancer: ResponseEnhancer | None = None,
166+
on_startup: Sequence[LifespanHook] | None = None,
167+
on_shutdown: Sequence[LifespanHook] | None = None,
157168
**factory_kwargs: Any,
158169
) -> Any:
159170
"""Build a Starlette ASGI app for in-process integration tests.
@@ -219,6 +230,15 @@ def build_asgi_app(
219230
every tool dispatch. Forwarded to :func:`create_mcp_server`.
220231
:param streaming_responses: Forwarded to :func:`create_mcp_server`.
221232
Default ``False``.
233+
:param stateless_http: Forwarded to :func:`create_mcp_server` for
234+
``transport="mcp"`` and to the production composition path for
235+
``transport="both"``. Ignored by ``transport="a2a"``.
236+
:param session_idle_timeout: Idle reap deadline for stateful MCP
237+
sessions. Defaults to 1800 seconds. Forwarded for ``"mcp"`` and
238+
``"both"``; ignored by ``"a2a"``.
239+
:param max_active_sessions: Optional cap for active stateful MCP
240+
sessions. Forwarded for ``"mcp"`` and ``"both"``; ignored by
241+
``"a2a"``.
222242
:param enable_dns_rebinding_protection: Forwarded to
223243
:func:`create_mcp_server`. ``None`` → FastMCP default.
224244
:param max_request_size: Request body size cap in bytes. ``None`` →
@@ -244,6 +264,10 @@ def build_asgi_app(
244264
:func:`create_mcp_server`, so in-process tests exercise the same
245265
enhancer wiring your production :func:`serve` call uses. ``None``
246266
→ no enhancer (default).
267+
:param on_startup: Async zero-argument hooks run after the MCP and A2A
268+
framework lifespans start. Requires ``transport="both"``.
269+
:param on_shutdown: Async zero-argument hooks run before the MCP and A2A
270+
framework lifespans stop. Requires ``transport="both"``.
247271
:param factory_kwargs: Forwarded to
248272
:func:`create_adcp_server_from_platform`. Accepted keys:
249273
``executor``, ``registry``, ``webhook_sender``,
@@ -257,6 +281,27 @@ def build_asgi_app(
257281
"""
258282
if transport not in ("mcp", "a2a", "both"):
259283
raise ValueError(f"Unsupported transport {transport!r}; expected 'mcp', 'a2a', or 'both'.")
284+
if (on_startup or on_shutdown) and transport != "both":
285+
raise ValueError(
286+
"on_startup / on_shutdown hooks require transport='both', "
287+
f"got transport={transport!r}."
288+
)
289+
if transport == "a2a":
290+
ignored_session_settings = []
291+
if stateless_http:
292+
ignored_session_settings.append("stateless_http")
293+
if session_idle_timeout != 1800.0:
294+
ignored_session_settings.append("session_idle_timeout")
295+
if max_active_sessions is not None:
296+
ignored_session_settings.append("max_active_sessions")
297+
if ignored_session_settings:
298+
warnings.warn(
299+
"build_asgi_app sets MCP-only session fields "
300+
f"{sorted(ignored_session_settings)} but transport='a2a'. "
301+
"These fields will be ignored.",
302+
UserWarning,
303+
stacklevel=2,
304+
)
260305

261306
from adcp.decisioning.serve import create_adcp_server_from_platform
262307
from adcp.server.serve import (
@@ -291,6 +336,9 @@ def build_asgi_app(
291336
advertise_all=advertise_all,
292337
max_request_size=max_request_size,
293338
streaming_responses=streaming_responses,
339+
stateless_http=stateless_http,
340+
session_idle_timeout=session_idle_timeout,
341+
max_active_sessions=max_active_sessions,
294342
validation=validation,
295343
pre_validation_hooks=pre_validation_hooks,
296344
response_enhancer=response_enhancer,
@@ -299,6 +347,8 @@ def build_asgi_app(
299347
allowed_origins=allowed_origins,
300348
enable_dns_rebinding_protection=enable_dns_rebinding_protection,
301349
auth=auth,
350+
on_startup=on_startup,
351+
on_shutdown=on_shutdown,
302352
include_discovery=discovery_base_url is not None,
303353
)
304354
return _apply_asgi_middleware(app, asgi_middleware)
@@ -331,6 +381,9 @@ def build_asgi_app(
331381
context_factory=context_factory,
332382
middleware=middleware,
333383
streaming_responses=streaming_responses,
384+
stateless_http=stateless_http,
385+
session_idle_timeout=session_idle_timeout,
386+
max_active_sessions=max_active_sessions,
334387
enable_dns_rebinding_protection=enable_dns_rebinding_protection,
335388
validation=validation,
336389
pre_validation_hooks=pre_validation_hooks,
@@ -373,12 +426,17 @@ async def build_test_client(
373426
context_factory: ContextFactory | None = None,
374427
middleware: Sequence[SkillMiddleware] | None = None,
375428
streaming_responses: bool = False,
429+
stateless_http: bool = False,
430+
session_idle_timeout: float | None = 1800.0,
431+
max_active_sessions: int | None = None,
376432
enable_dns_rebinding_protection: bool | None = None,
377433
max_request_size: int | None = None,
378434
validation: ValidationHookConfig | None = DEFAULT_VALIDATION,
379435
discovery_base_url: str | None = None,
380436
pre_validation_hooks: PreValidationHooks | None = None,
381437
response_enhancer: ResponseEnhancer | None = None,
438+
on_startup: Sequence[LifespanHook] | None = None,
439+
on_shutdown: Sequence[LifespanHook] | None = None,
382440
**factory_kwargs: Any,
383441
) -> AsyncIterator[httpx.AsyncClient]:
384442
"""Async context manager yielding an ``httpx.AsyncClient`` wired against
@@ -427,6 +485,9 @@ async def build_test_client(
427485
:param context_factory: Forwarded to :func:`build_asgi_app`.
428486
:param middleware: Forwarded to :func:`build_asgi_app`.
429487
:param streaming_responses: Forwarded to :func:`build_asgi_app`.
488+
:param stateless_http: Forwarded to :func:`build_asgi_app`.
489+
:param session_idle_timeout: Forwarded to :func:`build_asgi_app`.
490+
:param max_active_sessions: Forwarded to :func:`build_asgi_app`.
430491
:param enable_dns_rebinding_protection: Forwarded to
431492
:func:`build_asgi_app`.
432493
:param max_request_size: Forwarded to :func:`build_asgi_app`.
@@ -442,6 +503,10 @@ async def build_test_client(
442503
:param response_enhancer: Forwarded to :func:`build_asgi_app`. Wire
443504
the same enhancer your production :func:`serve` call uses so
444505
in-process tests exercise the enhancer path.
506+
:param on_startup: Forwarded to :func:`build_asgi_app`. Requires
507+
``transport="both"``.
508+
:param on_shutdown: Forwarded to :func:`build_asgi_app`. Requires
509+
``transport="both"``.
445510
:param factory_kwargs: Forwarded to
446511
:func:`create_adcp_server_from_platform` via :func:`build_asgi_app`
447512
(executor, registry, webhook_sender, etc.).
@@ -492,12 +557,17 @@ async def build_test_client(
492557
context_factory=context_factory,
493558
middleware=middleware,
494559
streaming_responses=streaming_responses,
560+
stateless_http=stateless_http,
561+
session_idle_timeout=session_idle_timeout,
562+
max_active_sessions=max_active_sessions,
495563
enable_dns_rebinding_protection=enable_dns_rebinding_protection,
496564
max_request_size=max_request_size,
497565
validation=validation,
498566
discovery_base_url=discovery_base_url,
499567
pre_validation_hooks=pre_validation_hooks,
500568
response_enhancer=response_enhancer,
569+
on_startup=on_startup,
570+
on_shutdown=on_shutdown,
501571
**factory_kwargs,
502572
)
503573
async with LifespanManager(app):

src/adcp/types/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@
716716
"UpdateContentStandardsSuccessResponse",
717717
"UpdateMediaBuyErrorResponse",
718718
"LegacyUpdateMediaBuyErrorResponse",
719+
"LegacyUpdateMediaBuySuccessResponse",
719720
"UpdateMediaBuyResponse1",
720721
"UpdateMediaBuyResponse3",
721722
"UpdateMediaBuyPackagesRequest",
@@ -845,6 +846,8 @@
845846
"SignalCoverageForecast",
846847
"SignalCoverageRange",
847848
"MissingMetric",
849+
"PropertyIdentifier",
850+
"ProductFilterCountry",
848851
# Cross-module name collision aliases (#911, Step 2)
849852
# Creative
850853
"DeliveryCreative",
@@ -1456,6 +1459,7 @@ def __dir__() -> list[str]:
14561459
LegacySyncCreativesRequest,
14571460
LegacyUpdateMediaBuyErrorResponse,
14581461
LegacyUpdateMediaBuyRequest,
1462+
LegacyUpdateMediaBuySuccessResponse,
14591463
ListAccountsRequest,
14601464
ListAccountsResponse,
14611465
ListCollectionListsRequest,
@@ -1567,13 +1571,15 @@ def __dir__() -> list[str]:
15671571
ProductCard,
15681572
ProductCardDetailed,
15691573
ProductCatalog,
1574+
ProductFilterCountry,
15701575
ProductFilters,
15711576
ProductFormatDeclaration,
15721577
ProductFormatSellerPreference,
15731578
ProductSignalTargetingOption,
15741579
Property,
15751580
PropertyId,
15761581
PropertyIdActivationKey,
1582+
PropertyIdentifier,
15771583
PropertyIdentifierTypes,
15781584
PropertyList,
15791585
PropertyListChangedWebhook,

src/adcp/types/_eager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@
599599
JavascriptFormatGroupAsset,
600600
KeyValueActivationKey,
601601
LegacyUpdateMediaBuyErrorResponse,
602+
LegacyUpdateMediaBuySuccessResponse,
602603
ListContentStandardsErrorResponse,
603604
ListContentStandardsResponse1,
604605
ListContentStandardsSuccessResponse,
@@ -628,8 +629,10 @@
628629
PreviewRenderingOrigin,
629630
PricingOption,
630631
ProductAllocation,
632+
ProductFilterCountry,
631633
ProductFormatSellerPreference,
632634
PropertyId,
635+
PropertyIdentifier,
633636
PropertyTag,
634637
Provenance,
635638
ProvenanceDeclaredBy,
@@ -1681,6 +1684,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
16811684
"UpdateFrequency",
16821685
"UpdateMediaBuyErrorResponse",
16831686
"LegacyUpdateMediaBuyErrorResponse",
1687+
"LegacyUpdateMediaBuySuccessResponse",
16841688
"UpdateMediaBuyPackagesRequest",
16851689
"UpdateMediaBuyPropertiesRequest",
16861690
"UpdateMediaBuyRequest",
@@ -1763,6 +1767,8 @@ def __init__(self, *args: object, **kwargs: object) -> None:
17631767
"PercentOfMediaVendorPricingOption",
17641768
"PerUnitVendorPricingOption",
17651769
"ProductAllocation",
1770+
"ProductFilterCountry",
1771+
"PropertyIdentifier",
17661772
"SignalCoverageForecast",
17671773
"SignalCoverageRange",
17681774
"TrustedMatch",

src/adcp/types/aliases.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,12 @@ class UnknownGroupAsset(_BaseGroupAsset):
20392039
from adcp.types.generated_poc.core.overlay import (
20402040
Unit as OverlayUnit,
20412041
)
2042+
from adcp.types.generated_poc.core.product_filters import (
2043+
Country as ProductFilterCountry,
2044+
)
2045+
from adcp.types.generated_poc.core.property import (
2046+
Identifier as PropertyIdentifier,
2047+
)
20422048
from adcp.types.generated_poc.core.provenance import (
20432049
DeclaredBy as ProvenanceDeclaredBy,
20442050
)
@@ -2099,6 +2105,9 @@ class UnknownGroupAsset(_BaseGroupAsset):
20992105
from adcp.types.generated_poc.media_buy.sync_event_sources_response import (
21002106
Setup as SyncEventSourcesSetup,
21012107
)
2108+
from adcp.types.generated_poc.media_buy.update_media_buy_response import (
2109+
UpdateMediaBuyResponse1 as LegacyUpdateMediaBuySuccessResponse,
2110+
)
21022111
from adcp.types.generated_poc.protocol.get_adcp_capabilities_response import (
21032112
Account as CapabilitiesAccount,
21042113
)
@@ -2266,6 +2275,8 @@ class UnknownGroupAsset(_BaseGroupAsset):
22662275
"SignalCoverageForecast",
22672276
"SignalCoverageRange",
22682277
"MissingMetric",
2278+
"PropertyIdentifier",
2279+
"ProductFilterCountry",
22692280
# Canonical-formats v2 surface (AdCP 3.1)
22702281
"CanonicalAssetSource",
22712282
"CanonicalCompositionModel",
@@ -2435,6 +2446,7 @@ class UnknownGroupAsset(_BaseGroupAsset):
24352446
"UpdateMediaBuySuccessResponse",
24362447
"UpdateMediaBuyErrorResponse",
24372448
"LegacyUpdateMediaBuyErrorResponse",
2449+
"LegacyUpdateMediaBuySuccessResponse",
24382450
"UpdateMediaBuyResponse3",
24392451
"UpdateMediaBuySubmittedResponse",
24402452
# Validate content delivery responses

0 commit comments

Comments
 (0)