Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7c87f88
feat: rewrite Targeting/FrequencyCap to extend AdCP library types (#1…
KonstantinMirin Feb 10, 2026
e8971f4
fix: clean up targeting_overlay types and storage key mismatch
KonstantinMirin Feb 10, 2026
46e3995
refactor: update non-GAM adapters to read v3 structured geo fields
KonstantinMirin Feb 10, 2026
2425718
refactor: rewrite overlay validation with explicit field-to-dimension…
KonstantinMirin Feb 10, 2026
4e70981
feat: add geo system validation to TargetingCapabilities
KonstantinMirin Feb 10, 2026
39eef9b
fix: reject city targeting in overlay validation instead of silently …
KonstantinMirin Feb 11, 2026
e87b0de
fix: harden legacy normalizer — bare regions, both-present guard, cit…
KonstantinMirin Feb 11, 2026
4bbb29f
feat: update GAM targeting manager for v3 structured geo fields
KonstantinMirin Feb 11, 2026
024a6c4
refactor: rename v2 flat geo fields to v3 structured names codebase-wide
KonstantinMirin Feb 11, 2026
4ac1e1f
fix: reject unknown targeting fields via model_extra inspection
KonstantinMirin Feb 11, 2026
02feda9
fix: default Targeting.model_dump to mode='json' and add v3 geo test …
KonstantinMirin Feb 11, 2026
a0692a3
refactor: remove unrelated chore changes from feature branch
KonstantinMirin Feb 11, 2026
8e753e7
feat: add geo inclusion/exclusion same-value overlap validation
KonstantinMirin Feb 11, 2026
5448123
fix: replace _had_city_targeting transient attr with proper Pydantic …
KonstantinMirin Feb 12, 2026
1a23caa
test: add geo overlap validation integration test
KonstantinMirin Feb 12, 2026
39c40dc
fix: override CreateMediaBuyRequest.packages to use local PackageRequest
KonstantinMirin Feb 12, 2026
61e888f
fix: add missing resolve_enum_value helper and fix mypy type errors
KonstantinMirin Feb 13, 2026
bc01c3c
chore: upgrade cryptography and pillow, ignore diskcache vuln
KonstantinMirin Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ jobs:
# GHSA-7gcm-g887-7qv7: protobuf DoS vulnerability (CVE-2026-0994)
# No fix available yet - affects all versions through 6.33.4
# Transitive dependency from google-ads, google-api-core, logfire, a2a-sdk
# Remove --ignore-vulns when protobuf releases a patched version
run: uvx uv-secure --ignore-vulns GHSA-7gcm-g887-7qv7
# GHSA-w8v5-vhqr-4h9v: diskcache vulnerability - no patch available
# Transitive dependency from fastmcp -> py-key-value-aio -> diskcache
# Tracked upstream: https://github.com/jlowin/fastmcp/issues/3166
run: uvx uv-secure --ignore-vulns GHSA-7gcm-g887-7qv7,GHSA-w8v5-vhqr-4h9v

smoke-tests:
name: Smoke Tests (Fast Import Checks)
Expand Down
4 changes: 2 additions & 2 deletions docs/adapters/mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ async def test_targeting_capabilities():
flight_start_date="2025-10-10",
flight_end_date="2025-10-11",
targeting_overlay={
"geo_country_any_of": ["US", "CA"],
"geo_region_any_of": ["CA", "NY"],
"geo_countries": ["US", "CA"],
"geo_regions": ["US-CA", "US-NY"],
"device_type_any_of": ["mobile", "tablet"],
"os_any_of": ["ios", "android"],
"browser_any_of": ["chrome", "safari"],
Expand Down
2 changes: 1 addition & 1 deletion docs/development/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async def get_products(
# AdCP Request
{
"targeting_overlay": {
"geo_country_any_of": ["US"],
"geo_countries": ["US"],
"signals": ["sports_enthusiasts", "auto_intenders"]
}
}
Expand Down
4 changes: 2 additions & 2 deletions docs/development/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,9 @@ Each adapter translates AdCP targeting to platform-specific format:
def _translate_targeting(self, overlay):
platform_targeting = {}

if "geo_country_any_of" in overlay:
if "geo_countries" in overlay:
platform_targeting["location"] = {
"countries": overlay["geo_country_any_of"]
"countries": overlay["geo_countries"]
}

if "signals" in overlay:
Expand Down
8 changes: 4 additions & 4 deletions examples/upstream_product_catalog_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
],
"targeting_template": {
"content_cat_any_of": ["sports", "basketball", "football"],
"geo_country_any_of": ["US", "CA"],
"geo_countries": ["US", "CA"],
},
"delivery_type": "guaranteed",
"is_fixed_price": False,
Expand All @@ -59,7 +59,7 @@
],
"targeting_template": {
"content_cat_any_of": ["finance", "business", "investing"],
"geo_country_any_of": ["US"],
"geo_countries": ["US"],
},
"delivery_type": "guaranteed",
"is_fixed_price": True,
Expand All @@ -80,7 +80,7 @@
],
"targeting_template": {
"content_cat_any_of": ["news", "politics", "world_news"],
"geo_country_any_of": ["US", "UK", "CA", "AU"],
"geo_countries": ["US", "UK", "CA", "AU"],
},
"delivery_type": "non_guaranteed",
"is_fixed_price": False,
Expand All @@ -98,7 +98,7 @@
"specs": {"title_length": 50, "description_length": 100},
}
],
"targeting_template": {"geo_country_any_of": ["US", "CA", "UK"]},
"targeting_template": {"geo_countries": ["US", "CA", "UK"]},
"delivery_type": "non_guaranteed",
"is_fixed_price": True,
"cpm": 8.0,
Expand Down
10 changes: 5 additions & 5 deletions examples/upstream_with_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"delivery_options": {"hosted": {}},
}
],
"targeting_template": {"geo_country_any_of": ["US", "CA", "UK", "AU"]},
"targeting_template": {"geo_countries": ["US", "CA", "UK", "AU"]},
"delivery_type": "non_guaranteed",
"is_fixed_price": True,
"cpm": 2.50,
Expand All @@ -56,7 +56,7 @@
"delivery_options": {"hosted": {}},
}
],
"targeting_template": {"geo_country_any_of": ["US", "CA", "UK", "AU"]},
"targeting_template": {"geo_countries": ["US", "CA", "UK", "AU"]},
"delivery_type": "non_guaranteed",
"is_fixed_price": True,
"cpm": 1.75,
Expand All @@ -82,7 +82,7 @@
}
],
"targeting_template": {
"geo_country_any_of": ["US", "CA", "UK", "AU"],
"geo_countries": ["US", "CA", "UK", "AU"],
"device_type_any_of": ["desktop", "tablet"], # Not great on mobile
},
"delivery_type": "non_guaranteed",
Expand All @@ -109,7 +109,7 @@
"delivery_options": {"hosted": {}},
}
],
"targeting_template": {"geo_country_any_of": ["US", "CA", "UK", "AU"]},
"targeting_template": {"geo_countries": ["US", "CA", "UK", "AU"]},
"delivery_type": "non_guaranteed",
"is_fixed_price": False,
"price_guidance": {"floor": 10.0, "p50": 15.0, "p75": 20.0},
Expand Down Expand Up @@ -139,7 +139,7 @@
"delivery_options": {"hosted": {}},
}
],
"targeting_template": {"content_cat_any_of": ["sports"], "geo_country_any_of": ["US"]},
"targeting_template": {"content_cat_any_of": ["sports"], "geo_countries": ["US"]},
"delivery_type": "guaranteed",
"is_fixed_price": False,
"price_guidance": {"floor": 8.0, "p50": 12.0, "p75": 15.0},
Expand Down
10 changes: 5 additions & 5 deletions scripts/setup/init_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def init_db(exit_on_error=False):
"id": "display_300x250",
}
],
targeting_template={"geo_country_any_of": ["US"]},
targeting_template={"geo_countries": ["US"]},
delivery_type="guaranteed",
is_fixed_price=False,
price_guidance={"floor": 10.0, "p50": 15.0, "p75": 20.0},
Expand Down Expand Up @@ -270,7 +270,7 @@ def init_db(exit_on_error=False):
],
"targeting_template": {
"content_cat_any_of": ["news", "politics"],
"geo_country_any_of": ["US"],
"geo_countries": ["US"],
},
"delivery_type": "guaranteed",
"is_fixed_price": False,
Expand All @@ -280,7 +280,7 @@ def init_db(exit_on_error=False):
"placement_ids": ["news_300x250_atf", "news_300x250_btf"],
"ad_unit_path": "/1234/news/display",
"key_values": {"section": "news", "tier": "premium"},
"targeting": {"content_cat_any_of": ["news", "politics"], "geo_country_any_of": ["US"]},
"targeting": {"content_cat_any_of": ["news", "politics"], "geo_countries": ["US"]},
},
},
{
Expand All @@ -293,7 +293,7 @@ def init_db(exit_on_error=False):
"id": "display_728x90",
}
],
"targeting_template": {"geo_country_any_of": ["US", "CA"]},
"targeting_template": {"geo_countries": ["US", "CA"]},
"delivery_type": "non_guaranteed",
"is_fixed_price": True,
"cpm": 2.5,
Expand All @@ -302,7 +302,7 @@ def init_db(exit_on_error=False):
"placement_ids": ["ros_728x90_all"],
"ad_unit_path": "/1234/run_of_site/leaderboard",
"key_values": {"tier": "standard"},
"targeting": {"geo_country_any_of": ["US", "CA"]},
"targeting": {"geo_countries": ["US", "CA"]},
},
},
]
Expand Down
68 changes: 67 additions & 1 deletion src/adapters/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import TYPE_CHECKING, Any, ClassVar

if TYPE_CHECKING:
from src.core.schemas import Targeting

from pydantic import BaseModel, ConfigDict, Field
from rich.console import Console
Expand Down Expand Up @@ -49,6 +54,67 @@ class TargetingCapabilities:
fr_code_postal: bool = False # French postal code
au_postcode: bool = False # Australian postcode

# Maps from AdCP enum value → dataclass field name.
_METRO_FIELDS: ClassVar[tuple[str, ...]] = (
"nielsen_dma",
"eurostat_nuts2",
"uk_itl1",
"uk_itl2",
)
_POSTAL_FIELDS: ClassVar[tuple[str, ...]] = (
"us_zip",
"us_zip_plus_four",
"gb_outward",
"gb_full",
"ca_fsa",
"ca_full",
"de_plz",
"fr_code_postal",
"au_postcode",
)

def validate_geo_systems(self, targeting: Targeting) -> list[str]:
"""Validate that targeting geo systems are supported by this adapter.

Checks both include and exclude fields for geo_metros and geo_postal_areas.
Returns list of errors naming the unsupported system and supported alternatives.
"""
from src.core.validation_helpers import resolve_enum_value

errors: list[str] = []

# Collect all metro items from include + exclude
metros: list[Any] = []
if targeting.geo_metros:
metros.extend(targeting.geo_metros)
if targeting.geo_metros_exclude:
metros.extend(targeting.geo_metros_exclude)

if metros:
supported = [f for f in self._METRO_FIELDS if getattr(self, f)]
for metro in metros:
system = resolve_enum_value(metro.system)
if not getattr(self, system, False):
alt = ", ".join(supported) if supported else "none"
errors.append(f"Unsupported metro system '{system}'. This adapter supports: {alt}")

# Collect all postal items from include + exclude
postals: list[Any] = []
if targeting.geo_postal_areas:
postals.extend(targeting.geo_postal_areas)
if targeting.geo_postal_areas_exclude:
postals.extend(targeting.geo_postal_areas_exclude)

if postals:
supported = [f for f in self._POSTAL_FIELDS if getattr(self, f)]
for area in postals:
system = resolve_enum_value(area.system)
if not getattr(self, system, False):
alt = ", ".join(supported) if supported else "none"
errors.append(f"Unsupported postal system '{system}'. This adapter supports: {alt}")

return errors


@dataclass
class AdapterCapabilities:
Expand Down
9 changes: 5 additions & 4 deletions src/adapters/gam/managers/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,16 +945,17 @@ def log(msg):
# AdCP: suppress_minutes (e.g., 60 = 1 hour)
# GAM: maxImpressions=1, numTimeUnits=X, timeUnit="MINUTE"/"HOUR"/"DAY"

# Determine best GAM time unit
# Determine best GAM time unit (int() cast needed because
# suppress_minutes is float after library type inheritance, GAM API expects int)
if freq_cap.suppress_minutes < 60:
time_unit = "MINUTE"
num_time_units = freq_cap.suppress_minutes
num_time_units = int(freq_cap.suppress_minutes)
elif freq_cap.suppress_minutes < 1440: # Less than 24 hours
time_unit = "HOUR"
num_time_units = freq_cap.suppress_minutes // 60
num_time_units = int(freq_cap.suppress_minutes // 60)
else:
time_unit = "DAY"
num_time_units = freq_cap.suppress_minutes // 1440
num_time_units = int(freq_cap.suppress_minutes // 1440)

frequency_caps.append(
{
Expand Down
Loading