Skip to content

refactor: enforce typed model boundaries across serialization and data flow#1044

Merged
ChrisHuie merged 51 commits intoprebid:mainfrom
KonstantinMirin:KonstantinMirin/chore-lint-and-formatting
Feb 18, 2026
Merged

refactor: enforce typed model boundaries across serialization and data flow#1044
ChrisHuie merged 51 commits intoprebid:mainfrom
KonstantinMirin:KonstantinMirin/chore-lint-and-formatting

Conversation

@KonstantinMirin
Copy link
Contributor

@KonstantinMirin KonstantinMirin commented Feb 13, 2026

Architecture: Typed Boundaries

Pydantic models are the data representation inside the application. Serialization to/from dicts and JSON happens only at system boundaries. No model_dump() between layers.

flowchart LR
    classDef boundary fill:#f0f8ff,stroke:#007bff,stroke-width:2px,stroke-dasharray: 5 5,color:#000;
    classDef internal fill:#28a745,stroke:#28a745,stroke-width:2px,color:#fff;
    classDef io fill:#333,stroke:#333,stroke-width:2px,color:#fff;

    subgraph Input_Boundary [Boundary: Coercion]
        direction TB
        P_Parse(Pydantic Parse):::boundary
        P_Validate(model_validate):::boundary
    end

    subgraph Internal_Domain [Internal: Typed Models]
        direction TB
        M_Request(CreateMediaBuyRequest):::internal
        M_Pkg(Package / Targeting):::internal
        M_Prod(Product / Creative):::internal
    end

    subgraph Output_Boundary [Boundary: Serialization]
        direction TB
        S_Dump("model_dump(mode='json')"):::boundary
        S_Tool(ToolResult):::boundary
        S_Serial(json_serializer):::boundary
    end

    Buyer[Buyer JSON]:::io
    DB_Read[(DB Read)]:::io
    DB_Write[(DB JSONB)]:::io
    Response[A2A / API Response]:::io

    Buyer --> P_Parse --> M_Request --> S_Dump
    M_Pkg --> S_Serial --> DB_Write
    DB_Read --> P_Validate --> M_Prod --> Response
    S_Dump -.-> S_Tool
Loading

Before: Models were frequently converted to dicts mid-flow (model_dump()) then reconstructed, creating opportunities for field loss, type confusion, and silent bugs. Internal functions accepted dict[str, Any] or Any where typed models should have been used.

After: Every internal function accepts and returns typed Pydantic models. Serialization happens exactly twice — once at input (coercion) and once at output (protocol/DB). The model_dump() calls that previously peppered the business logic have been removed or pushed to the boundaries.

Library type inheritance

All buyer-facing models extend adcp library types. Extension classes exist only for:

  1. model_config overrideConfigDict(extra=get_pydantic_extra_mode()): forbid extras in dev, ignore in prod
  2. Codegen gaps — spec fields the code generator missed (e.g., creative_ids on PackageUpdate). Filed upstream, removed once fixed
  3. Internal-only fields — not in the spec, marked with Field(exclude=True)
  4. Normalizing validators — pre-validators translating deprecated protocol fields to current structure

What Changed

Model Hierarchy Unification

  • SalesAgentBaseModel now extends the adcp library's AdCPBaseModel, inheriting exclude_none=True, model_summary(), and JSON-mode serialization. Replaces the previous parallel AdCPBaseModel that reimplemented these behaviors with a custom __init__ hack
  • All ~90 internal models migrated from BaseModel/AdCPBaseModelSalesAgentBaseModel
  • Environment-aware validation (extra='forbid' in dev, extra='ignore' in prod) now uses native Pydantic model_config instead of runtime __init__ override

Serialization Boundaries

  • Database engine: Registered pydantic_core.to_json as json_serializer on SQLAlchemy engine — all JSONB columns now handle Pydantic models, enums, AnyUrl, and datetimes automatically
  • JSONType: Updated to accept BaseModel instances directly, eliminating premature model_dump() before DB writes
  • Protocol responses: Added mode='json' at MCP, A2A, and webhook serialization boundaries
  • Nested serialization: NestedModelSerializerMixin and 5 custom @model_serializer methods propagate mode=info.mode to child models

Media Buy Tool Refactoring

_create_media_buy_impl (3,800-line file, 680-line diff): Replaced 14 untyped parameters with a single typed CreateMediaBuyRequest. Removed 6 dead parameters never passed by either caller. Eliminated cast(list[Package], packages) workarounds — functions now accept list[MediaPackage] directly.

_update_media_buy_impl: Replaced 17 untyped parameters with a single typed UpdateMediaBuyRequest. MCP wrapper no longer model_dump()s typed models — they flow directly through. Budget/date pre-processing extracted to shared _build_update_request() helper.

Schema extensions for both:

  • UpdateMediaBuyRequest extends library's UpdateMediaBuyRequest1 — only adds budget (convenience), today (testing), and field overrides for oneOf/packages
  • AdCPPackageUpdate extends library's PackageUpdate1 — only adds creative_ids (spec field missing from codegen)

Dict-to-Model Migrations

  • PolicyCheckService: Accepts Product and BrandManifest models instead of dicts. Removed dead age-based eligibility logic that referenced non-existent fields
  • ranking_agent: Accepts list[Product] instead of list[dict], removed make_json_serializable() hack
  • ToolContext.testing_context: Typed as AdCPTestContext instead of dict, eliminating model→dict→model roundtrips
  • apply_testing_hooks: Returns structured TestingHooksResult dataclass instead of mutating a data dict. Eliminated 90+ lines of dict surgery across 3 callers
  • Adapter targeting: 4 adapter files now use typed Targeting model attributes instead of dict key access. Fixed a functional bug in Xandr's _create_targeting_profile where wrong nested field names (targeting["geo"]["countries"]) never matched the flat structure (geo_country_any_of), silently producing empty targeting profiles

Buyer-Facing Input Validation

  • 7 buyer-facing request models now override model_config with extra='forbid' — preventing arbitrary field injection that was previously silently accepted, survived model_dump(), and got stored in the database
  • CreateMediaBuyRequest.packages and PackageRequest.targeting_overlay override library types to use our extended versions with extra='forbid'

Creative Sync: Package Decomposition

src/core/tools/creatives.py (2,584 lines, 16 functions) decomposed into a Python package with 9 focused submodules:

Module Responsibility Key functions
_sync.py Orchestrator — main sync loop _sync_creatives_impl
_validation.py Input validation, field extraction _validate_creative_input, _get_field
_processing.py Create/update individual creatives _create_new_creative, _update_existing_creative
_assets.py URL extraction, creative data building _extract_url_from_assets, _build_creative_data
_assignments.py Creative-to-package assignments _process_assignments
_workflow.py Workflow steps, notifications, audit _create_sync_workflow_steps, _send_creative_notifications
listing.py Creative discovery and filtering _list_creatives_impl, list_creatives
sync_wrappers.py MCP and A2A entry points sync_creatives, sync_creatives_raw
__init__.py Backward-compatible re-exports All public functions

Backward compatibility preserved via __init__.py re-exports. Prior to the package conversion, _sync_creatives_impl was incrementally decomposed across 7 commits.

Integration Test Fixes

Several integration tests were silently passing with incorrect data due to the old extra='allow'/extra='ignore' defaults:

  • PrincipalSchema construction (inventory.py): Was passing tenant_id and access_token which aren't Principal fields — silently accepted before, now caught by extra='forbid'. Fixed by removing the extra fields
  • _update_media_buy_impl callers: 5 test call sites were passing exploded kwargs that no longer match the new typed req=UpdateMediaBuyRequest(...) signature. Updated to construct proper request objects
  • FormatIdMatcher mock class: Integration tests used a custom matcher class with __eq__ that relied on str(FormatId) — but the library FormatId has no custom __str__, so comparisons silently failed. Replaced with real FormatId model instances from the adcp library
  • PromotedOfferings test data: Was {"name": "...", "description": "..."} which doesn't conform to the typed schema (requires brand_manifest). Fixed to valid PromotedOfferings structure

Lint and Type Check Cleanup

  • Removed 12 redundant # noqa comments suppressing globally-ignored or non-existent rules
  • Fixed duplicate targeting_overlay field definition in PackageRequest
  • mypy is now fully clean (0 errors) across all 224 source files
  • 24 unnecessary type: ignore comments removed after enabling pydantic.mypy plugin

Code Removal

  • ~170 lines of dead schema validation code (SchemaMetadata, ResponseWithSchema, enhance_*)
  • Redundant model_dump() calls removed from 9 ToolResult sites (FastMCP already handles serialization)

Quality Infrastructure

  • Added Makefile with quality, pre-pr, lint-fix, typecheck targets
  • Enabled C90 (complexity) and PLR (refactor) ruff rules

Impact

  • 146 files changed, net ~1,000 lines added (mostly test improvements and package decomposition)
  • All existing tests pass with updates to respect extra='forbid'
  • No public API changes — all changes are internal to serialization and data flow

Known Limitations

  • Targeting type boundary: PackageRequest.targeting_overlay accepts our local Targeting model, but the library's Package expects TargetingOverlay. Structurally different types (flat geo_country_any_of vs structured geo_countries). Test marked xfail — proper fix requires making Targeting inherit from TargetingOverlay (separate task).

Test Results

Suite Result
Unit tests 1,986 passed, 11 skipped
Integration 530 passed, 36 skipped, 1 xfailed
Integration V2 174 passed, 10 deselected
E2E 73 passed, 8 skipped, 3 xfailed

Test Plan

  • make quality — formatting, linting, mypy (0 errors), unit tests
  • ./run_all_tests.sh ci — full CI suite with Docker + PostgreSQL (all suites green)
  • Verify backward compatibility of creatives/ package re-exports
  • Verify extra='forbid' rejects unknown fields in dev mode
  • Verify extra='ignore' accepts unknown fields in production mode (dynamic model test)

KonstantinMirin and others added 30 commits February 12, 2026 15:37
- Add Makefile with quality, pre-pr, lint-fix, typecheck, test-fast targets
- Enable C90 (mccabe complexity) and PLR (pylint refactor) ruff rules
- Add justification comments to all lint suppressions
- Remove stale UP038 rule (removed from ruff)
- Suppress structural PLR rules (0911/0912/0913/0915) for incremental fix
- PLR5501 (collapsible-else-if) and other fixable rules left enforced
61 files reformatted. No logic changes — formatting only.
Auto-fixed:
- PLR5501: 12 collapsible else-if blocks simplified to elif
- PLR1711: 1 useless return removed
- PLR1730: 1 if-stmt replaced with min/max

Manual fixes:
- PLR0133: "active" == False always evaluates to False, making it
  a dead value in the truthy-check tuple. Replaced with "active".
- PLR1714: 3 repeated equality comparisons merged into `in` checks

Suppressed (structural, fix incrementally):
- C901: 216 complex functions
…on engine

Phase 1 of j55: Fix serialization boundaries that produce non-JSON-safe
output from model_dump() (Enum objects, datetime, AnyUrl, etc.).

- database_session.py: Register pydantic_core.to_json as json_serializer
  on both create_engine() calls so all JSONB columns handle Pydantic types
- protocol_envelope.py: model_dump(mode='json') at payload serialization
- mcp_server_enhanced.py: model_dump(mode='json') at response serialization
- adcp_a2a_server.py: model_dump(mode='json') at all 12 boundary sites
…content

Phase 2 of j55: FastMCP's ToolResult already calls
pydantic_core.to_jsonable_python() on structured_content, so passing
Pydantic models directly is safe and avoids redundant serialization.

- Remove model_dump() from 9 ToolResult sites across 7 tool files
- Add mode='json' to 1 composite ToolResult in media_buy_create.py
- Fix implementation-aware test that used MagicMock with
  model_dump.return_value instead of a real Pydantic model
Phase 4 of j55: Fix NestedModelSerializerMixin and 5 custom
@model_serializer methods to pass mode=info.mode when calling
model_dump() on nested Pydantic models.

Previously, nested models always serialized in Python mode regardless
of the parent's requested mode. Now mode='json' propagates correctly
through the entire model tree, ensuring Enum/datetime/AnyUrl values
are JSON-safe when the parent requests JSON mode.

The Creative.model_dump() status enum normalization is preserved as
intentional behavior — Creative status is always returned as a string
regardless of serialization mode.
Phase 4 of j55: Fix NestedModelSerializerMixin and 5 custom
@model_serializer methods to pass mode=info.mode when calling
model_dump() on nested Pydantic models.

Previously, nested models always serialized in Python mode regardless
of the parent's requested mode. Now mode='json' propagates correctly
through the entire model tree, ensuring Enum/datetime/AnyUrl values
are JSON-safe when the parent requests JSON mode.

The Creative.model_dump() status enum normalization is preserved as
intentional behavior — Creative status is always returned as a string
regardless of serialization mode.
PolicyCheckService methods accepted dicts instead of typed models,
forcing callers to call model_dump() and losing type safety.

- check_brief_compliance: brand_manifest dict → BrandManifest | str | None
- check_product_eligibility: product dict → Product
- Remove dead age-based eligibility logic (targeted_ages/verified_minimum_age
  fields don't exist on Product, so the code always hit an unintended path
  that rejected all products for age-restricted advertisers)
- Remove unnecessary model_dump() calls in products.py callers
- Add 8 unit tests, update 10 integration tests
model_dump() silently drops exclude=True fields, so base format's
platform_config was always lost when applying product overrides.
Only the override's platform_config survived the merge.

Replace dict roundtrip (model_dump + Format(**dict)) with direct
model attribute access + model_copy(update=...).
Replace 14 untyped/Any params with a single typed CreateMediaBuyRequest.
Both MCP and A2A wrappers now construct the request at the boundary,
eliminating Pydantic→dict→Pydantic roundtrips. Remove 6 dead params
(pacing, daily_budget, creatives, required_axe_signals,
enable_creative_macro, strategy_id) never passed by either caller.

Internal model_dump() calls replaced with direct typed access:
- PackageRequest→Package: direct construction instead of dict roundtrip
- DB storage: pass Pydantic models directly (engine json_serializer handles)
- Response packages: direct Package construction instead of manual dicts
- Testing hooks: simplified to model_dump() (removed dead model_dump_internal check)
…m:KonstantinMirin/prebid-salesagent into KonstantinMirin/chore-lint-and-formatting

# Conflicts:
#	src/core/schemas.py
…l_dump

The _pydantic_json_serializer on the DB engine and
pydantic_core.to_jsonable_python() in ToolResult already handle
enum serialization at boundaries. No need to normalize in model_dump.
…oksResult

Return structured metadata instead of mutating a data dict. Eliminates
all dict roundtrips in callers: dead code in products.py, 58-line dict
surgery in media_buy_delivery.py, 33-line reconstruction in
media_buy_create.py. Only surviving mutation (media_buy_id test_ prefix)
now uses model_copy.

Co-Authored-By: Constantine Mirin <constantine.mirin@gmail.com>
- Update JSONType.process_bind_param to accept BaseModel instances,
  completing the deferred Phase 3 from the serialization architecture.
  The engine's pydantic_core.to_json serializer handles them correctly.
- Remove ~170 lines of dead code from schema_validation.py (SchemaMetadata,
  ResponseWithSchema, enhance_*, validate_response_against_schema, etc.)
- Eliminate premature model_dump() in adapters.py save_adapter_config,
  keeping the validated Pydantic model flowing through to the DB write.
… of dict

Eliminates model→dict→model roundtrip: sources no longer model_dump(),
downstream consumers no longer reconstruct from dict. Removes dead
get_test_header() method and unused BaseModel import.

Co-Authored-By: Constantine Mirin <konstantin.mirin@gmail.com>
- ranking_agent.py: accept list[Product] instead of list[dict], use
  direct attribute access, remove make_json_serializable() hack
- products.py: pass eligible_products directly instead of model_dump()
- factory.py: keep ModelSettings model instead of calling model_dump()

Co-Authored-By: Konstantin Mirin <konstantin.mirin@anthropic.com>
Add pydantic.mypy plugin to mypy.ini, fixing 21 false-positive errors
where mypy didn't understand Field(None, ...) defaults. Fix real bug
where pkg_data was undefined (should be pkg_obj). Widen function
signatures to accept list[MediaPackage] matching actual data flow.
Remove 24 now-unnecessary type: ignore comments across 8 files.
…icts

Remove model_dump() calls across 4 adapter files, replacing dict access
with typed Pydantic model attribute access. Fixes a functional bug in
Xandr's _create_targeting_profile which used wrong nested field names
(targeting["geo"]["countries"]) that never matched Targeting's flat
structure (geo_country_any_of), silently producing empty profiles.
Override model_config with get_pydantic_extra_mode() on 7 buyer-facing
models that inherited extra='allow' from the adcp library. This prevents
buyer injection of arbitrary fields that were silently accepted, survived
model_dump(), and got stored in DB.

- Replace AdCPBaseModel __init__ hack with native Pydantic model_config
- Add model_config override on CreateMediaBuyRequest, PackageRequest,
  Targeting, Creative, ListCreativeFormatsRequest, ListCreativesRequest,
  GetMediaBuyDeliveryRequest
- Override packages type on CMR and targeting_overlay on PackageRequest
  to use our extended types instead of library's
- Fix Creative's validate_format_id to pop 'format' alias key
- Fix all Creative construction sites using legacy created_at/updated_at
- Add Literal return type to get_pydantic_extra_mode() for mypy
…brary base

Replace parallel AdCPBaseModel with SalesAgentBaseModel(adcp.types.base.AdCPBaseModel).
All ~90 internal models now inherit from SalesAgentBaseModel, gaining:
- exclude_none=True (from library base)
- model_summary() (from library base)
- extra='forbid' in dev (from SalesAgentBaseModel)

Removes duplicate model_dump/model_dump_json overrides (library provides these).
Fixes tests passing invalid extra fields (buyer_ref, has_more) that were
silently accepted under extra='ignore'.
…orting_webhook)

GetProductsRequest: add product_selectors (PromotedProducts) and pagination fields.
UpdateMediaBuyRequest: add reporting_webhook (ReportingWebhook) field.
Fix test helpers to generate valid example values for $ref schema types.
- Remove redundant `from src.core.schemas import Package` inside conditional
  block that shadowed the module-level import (caused "cannot access local
  variable 'Package'" at runtime)
- Remove invalid extra fields from test fixtures:
  - Principal: drop tenant_id, access_token (not in schema)
  - SyncCreativeResult: drop buyer_ref (not in schema)
  - Creative: use created_date/updated_date instead of created_at/updated_at
- Accept 500 status in analyze-ad-server route test (expected in test env)
FastMCP coerces input dicts to the type hint in the function signature.
Using AdcpPackageRequest (library parent) meant instances couldn't be
assigned to CreateMediaBuyRequest.packages which expects our local
PackageRequest (child type) — Pydantic rejects parent→child assignment.
- Update 7 tests from old dict-mutation API to new TestingHooksResult API
- Fix PackageRequest test to use local Targeting model (not library TargetingOverlay)
- Mark targeting_overlay type boundary test as xfail (pre-existing gap)
- Update pre-commit hook regex for new apply_testing_hooks signature
- Add __test__ = False to TestingHooksResult to prevent pytest collection warning
…-lint-and-formatting

# Conflicts:
#	src/adapters/xandr.py
#	src/core/schemas.py
#	src/core/tools/media_buy_create.py
#	tests/integration/test_list_creative_formats_params.py
#	tests/integration_v2/test_create_media_buy_v24.py
#	tests/unit/test_adcp_contract.py
…PR merge)

Cherry-picked from feature/agentic-coding-improvements for local development.
These files must be removed or squashed out before merging.
…pdate

Replace parallel schema classes with minimal library extensions:
- AdCPPackageUpdate extends PackageUpdate1, only adds creative_ids (codegen gap)
- UpdateMediaBuyRequest extends UpdateMediaBuyRequest1, inherits all AdCP fields
- _update_media_buy_impl accepts typed req object (matches create pattern)
- Remove model_dump() calls from MCP wrapper — typed models flow directly
- Extract _build_update_request() helper for budget/date pre-processing
…dry-run path

changes_applied is Field(exclude=True) and would_update is write-only,
so storing the typed model instead of a dict has zero downstream impact.
principal_data was assigned via model_dump() but never referenced.
The Principal model is used directly downstream (get_adapter, etc).
Remove model_dump(exclude_none=True) and dead hasattr guard for
targeting_overlay in update path. Engine's pydantic_core.to_json
serializer handles Pydantic models natively in JSONB columns.
…icts

validate_overlay_targeting and validate_geo_overlap now accept Targeting
Pydantic models directly. This makes validation actually effective —
the previous dict-based approach missed managed-only fields (excluded by
model_dump) and removed fields (consumed by the normalizer). Removed
model_dump() call in media_buy_create.py caller.
Store Pydantic models directly in package_config JSONB dict instead of
pre-serializing via model_dump(). The engine's pydantic_core.to_json
serializer handles Pydantic models natively in JSONB columns.
Remove model_dump(mode="json") + hasattr guard + cast(dict) fallback
at the call site in media_buy_update.py. The impl already handles both
dicts and models at line 98-100. Simplify assignments dict to use
.creative_id directly since pkg_update.creatives is typed list[CreativeAsset].
…s_impl

Moves the ~85-line validation block (schema_data construction, Creative
model validation, business logic checks, format registry validation)
into a standalone _validate_creative_input() function. No behavior
change — all 1961 tests pass unchanged.
…s_impl

DRY up duplicated URL extraction logic (update path and create path)
into a standalone pure function with tests.
DRY: consolidate duplicated data dict construction (~15 lines each) from
update and create paths into a single pure helper function.  Also fixes
missing snippet/snippet_type handling in the update path and missing
context in the create path.
…es_impl

Moves the entire update path (~450 lines) into a standalone helper function
that handles field updates, approval mode logic, creative agent validation
(generative and static), preview extraction, and data persistence. Returns
(SyncCreativeResult, needs_approval) tuple for the caller to handle tracking.
Move the entire create-path (~410 lines) from inside the savepoint loop
into a standalone helper function. Covers new creative DB insertion,
format validation, creative agent processing (generative build and static
preview), approval mode logic, and AI review submission.

Return type is tuple[SyncCreativeResult, bool] (result, needs_approval)
matching the _update_existing_creative pattern. Caller handles all
tracking (failed_creatives, created_count, creatives_needing_approval).
Moves ~200 lines of assignment processing logic (package lookup, format
validation, idempotent upsert, media-buy status transitions, and results
population) into a standalone _process_assignments() helper. The caller
now delegates via a single function call.

Part of the DRY refactoring chain for creatives.py.
…c_creatives_impl

Extract three helpers from _sync_creatives_impl post-processing section:
- _create_sync_workflow_steps: workflow step creation with push_notification_config
- _send_creative_notifications: Slack notification logic for pending/rejected creatives
- _audit_log_sync: both AdCP-level and principal-level audit log entries

The orchestrator now reads as a clear sequence of delegated calls.
Phase 1a of dict-to-model retyping chain. Changed _sync_creatives_impl
signature to accept Sequence[CreativeAsset | BaseModel | dict] with typed
PushNotificationConfig and ContextObject params. Removed model_dump at
orchestrator level — models now flow through to helpers which convert
via transitional isinstance guards. Updated MCP wrapper and
creative_helpers to pass models directly instead of converting to dicts.
…e access

Phase 1b/1c/1d: Remove isinstance guards from 5 extracted helpers
and replace 65 .get() calls with direct CreativeAsset attribute access.
Normalize dicts to CreativeAsset at orchestrator loop entry.
Handle typed Asset models in URL extraction and asset iteration.
…cation_config flow

- Update sync_creatives_raw type annotations to accept CreativeAsset, PushNotificationConfig, ContextObject
- Construct typed models at A2A boundary in _handle_sync_creatives_skill
- Stop destructuring PushNotificationConfig into dict in task_metadata; pass model through
- Update _send_protocol_webhook to use model attribute access instead of dict .get()
- Normalize push_notification_config to dict in _create_media_buy_impl for downstream compat
Split the monolithic 2584-line creatives.py into a proper Python package
with 9 focused submodules: _sync.py (orchestrator), _validation.py,
_processing.py, _assets.py, _assignments.py, _workflow.py, listing.py,
sync_wrappers.py, and __init__.py (backward-compatible re-exports).

Updated mock.patch targets in 4 test files to reference the correct
submodule where each dependency is imported.
…overlay

Remove noqa comments suppressing globally-ignored rules (E402, F841),
non-enabled rules (BLE001), and non-existent violations (F823).
Fix duplicate targeting_overlay field definition in PackageRequest
that caused mypy no-redef and unused-ignore errors.
Replace getattr(option, "root", option) with option.root in
dynamic_pricing_service.py and products.py — adcp 3.2.0 always
has .root, giving mypy the proper union type and eliminating 5
union-attr ignores. Replace type: ignore with cast(Any, ...) for
MinimalContext duck-typing in adcp_a2a_server.py. Remove stale
comments from media_buy_status_scheduler.py.
The file was silently excluded by .gitignore rule `sync_*.py` (intended
for one-off diagnostic scripts). Added `!src/**/sync_*.py` exception to
allow source files matching that pattern. This was the root cause of all
CI import failures after the creatives package conversion.
- Re-export get_principal_id_from_context and get_current_tenant from
  creatives/__init__.py for mock.patch backward compatibility
- Revert CreativeAsset(**c) conversion in A2A handler — library type
  requires FormatId model but callers pass format_id as plain string;
  _sync_creatives_impl already handles raw dicts
- Serialize FormatId to str in workflow step data to prevent
  "Object of type FormatId is not JSON serializable" in JSONB columns
- Update tests to use new _create_media_buy_impl(req=) and
  _update_media_buy_impl(req=) signatures instead of exploded kwargs
- Remove stale xfail on targeting_overlay test (now passes)
- Remove extra kwargs from Principal in smoke test (extra="forbid")
…essions

- Remove model_dump() calls in _sync.py and _workflow.py — pass FormatId
  and other Pydantic models directly (engine's _pydantic_json_serializer
  handles JSONB serialization automatically)
- Add json_serializer to integration test conftest engine (required now
  that assets contain typed Pydantic models instead of plain dicts)
- Replace hacky FormatIdMatcher test helper with real FormatId models
  from the adcp library — tests now use the same typed objects as
  production code
- Fix _update_media_buy_impl test calls to use req=UpdateMediaBuyRequest()
  pattern matching the refactored function signature
- Fix promoted_offerings test data to conform to typed PromotedOfferings
  schema (requires brand_manifest field)
- Revert HTTP 500 from acceptable status codes in admin UI route test
- Add TestProductionModeBehavior with dynamic model class (old test was
  broken on main — model_config baked at class definition time)
- Fix city targeting to assert exact violation message
- Add model_extra is None assertions proving extra='forbid' is active
- Add TODO(adcp-lib) tracking comments on drift allowlists
- Add PromotedOfferings type check to generative creatives test
- Fix mypy error: annotate failed_creatives as list[dict[str, Any]]
PrincipalSchema only has principal_id, name, platform_mappings.
tenant_id and access_token were silently accepted when Principal
extended BaseModel (extra='ignore'), but now fail with extra='forbid'.
@ChrisHuie ChrisHuie merged commit c412ce9 into prebid:main Feb 18, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments