Skip to content

feat(decisioning): createOAuthPassthroughResolver — Shape B account resolver factory#472

Merged
bokelley merged 2 commits into
mainfrom
bokelley/feat-create-oauth-passthrough-resolver
May 3, 2026
Merged

feat(decisioning): createOAuthPassthroughResolver — Shape B account resolver factory#472
bokelley merged 2 commits into
mainfrom
bokelley/feat-create-oauth-passthrough-resolver

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 3, 2026

Summary

Port of @adcp/sdk@6.7's createOAuthPassthroughResolver (Shape B
account-resolver factory). Standardises the canonical pattern where an
adapter wraps a vendor OAuth + /me/adaccounts-shaped API (Snap, Meta,
TikTok-shaped) and resolves the buyer's AccountReference by hitting
the upstream's listing endpoint with the buyer's bearer.

Without this factory, every Shape B adopter copies the same ~30 LOC:
extract bearer from ctx.auth_info, GET /me/adaccounts, match by id,
return the mapped Account.

Design decisions / divergences from JS

  • extract_rows callback instead of JS's rowsPath string. JS
    ships rowsPath: 'data' | string | null (single-segment only) and
    documents that deeper-nested shapes — TikTok's data.list — need a
    custom fetch wrapper. Python takes a callable defaulting to flat-list
    or {"data": [...]}, so deeper nests are a one-liner.
  • No in-memory caching layer ported. The JS factory ships an
    optional LRU+TTL listing cache. The Python issue spec (feat(server): createOAuthPassthroughResolver — Shape B account resolver factory #458) doesn't
    call for it; deferring to a follow-up keeps this PR scoped to the
    resolve primitive. Cache composition can layer on cleanly via
    compose_method once we need it.
  • Bearer refresh via DynamicBearer. Upstream client already
    handles auth injection; the factory just forwards ctx.auth_info
    (overridable via get_auth_context) to get_token.
  • Errors propagate verbatim. The upstream client already projects
    non-2xx → spec-conformant AdcpError codes (AUTH_REQUIRED,
    SERVICE_UNAVAILABLE, etc.). 401 surfaces as AUTH_REQUIRED; 404 on
    the listing endpoint becomes None (the client's
    treat_404_as_none default), which the factory treats as no rows.

Tests cover

  • ref by id matches an upstream row → returns mapped Account
  • ref by id with no match → returns None
  • natural-key ref / None ref → returns None without calling upstream
  • upstream 401 → AdcpError(AUTH_REQUIRED) surfaces
  • upstream 500 → AdcpError(SERVICE_UNAVAILABLE) surfaces
  • upstream 404 → None (no rows)
  • custom id_field (e.g. account_uuid)
  • custom extract_rows receives the raw parsed body (TikTok-shape data.list)
  • default extract_rows handles flat-list-shaped APIs
  • async + sync to_account both work
  • default get_auth_context forwards auth_info verbatim
  • custom get_auth_context threads through to DynamicBearer.get_token
  • raw-dict ref (legacy JSON-deserialised callsites) still resolves

Test plan

  • ruff check on changed files
  • mypy src/adcp/decisioning/oauth_passthrough.py
  • pytest tests/test_oauth_passthrough.py -v (15/15 pass)
  • pytest tests/ -x regression (3375 passed, 0 failed)

Refs #458, parent #452.

🤖 Generated with Claude Code

bokelley added a commit that referenced this pull request May 3, 2026
… AccountStore-shaped object

Address PR #472 review:

- BLOCKER 1: change resolver signature to (ref, auth_info=None) to match
  AccountStore.resolve Protocol. Adopters can now plug the result directly
  into DecisioningPlatform.accounts. Internally synthesise a ResolveContext
  for to_account / get_auth_context callbacks so adopter callbacks stay
  uniform with the rest of the AccountStore surface.
- BLOCKER 2: return an AccountStore-shaped class (resolution='explicit',
  resolve method) instead of a bare callable. Module docstring + example
  updated to show wiring under DecisioningPlatform.accounts.
- Layering: tests now import AccountReferenceById from adcp.types instead
  of reaching into generated_poc/.
- Pagination (option b): documented limitation in module docstring +
  resolver docstring. Async-iterator support deferred to follow-up to keep
  fix-pack scope tight.
- Soften platform names in module docstring; replace brand placeholders
  in tests with generic "Globex".
- Move _default_extract_rows / _default_auth_context above the public
  factory for read-order; add direct unit tests for _default_extract_rows
  edge cases (None body, {}, {"data": null}, {"data": "not_a_list"},
  flat list, data envelope).
@bokelley bokelley force-pushed the bokelley/feat-create-oauth-passthrough-resolver branch from b2245a1 to 1910046 Compare May 3, 2026 13:35
bokelley added 2 commits May 3, 2026 09:40
…esolver factory

Port of @adcp/sdk@6.7's `createOAuthPassthroughResolver` (issue #458).
Standardises the canonical "Shape B" account-resolution pattern: an
adapter wraps a vendor OAuth + ad-account API (Snap, Meta, TikTok-shaped)
and resolves the buyer's `AccountReference` by hitting the upstream's
`/me/adaccounts`-shaped listing endpoint with the buyer's bearer.

Behaviour:
- Only the `{account_id}` discriminated-union arm is handled; natural-key
  refs and `None` return `None` without calling upstream.
- Bearer pass-through via `DynamicBearer` on the upstream client; the
  factory forwards `ctx.auth_info` to `get_token` by default.
- Upstream errors propagate verbatim — the upstream client already
  projects non-2xx to spec-conformant `AdcpError` codes.
- `extract_rows` is a callable (Python diverges from JS's `rowsPath`
  string) defaulting to flat-list-or-`{"data": []}`. Adopters with
  deeper-nested shapes pass their own callback.

Refs #458, parent #452.
…untStore-shaped object

Address code-review BLOCKERS: returned resolver had a (ref, ctx)
signature that wouldn't match the framework dispatcher (which calls
resolve with auth_info kwarg), and the docstring example referenced
a phantom ExplicitAccounts(resolve=) pattern that doesn't exist.

- Factory returns _OAuthPassthroughAccountStore: a class whose
  resolve(ref, auth_info=None) matches the Protocol. Adopters wire
  the returned object directly into DecisioningPlatform.accounts.
- ResolveContext is synthesized inside resolve so adopter callbacks
  (to_account, get_auth_context) keep the ctx-based API uniform with
  upsert/list/sync_governance.
- Tests now import AccountReferenceById from adcp.types instead of
  reaching into generated_poc/ (CLAUDE.md layering rule).
- Test brand placeholders changed to Globex.
- Pagination limitation documented in module + factory docstrings:
  single GET, paginated upstreams must aggregate inside extract_rows
  or compose their own resolver. Async-iterator support is a
  follow-up.
- Six unit tests added for _default_extract_rows edge cases.
@bokelley bokelley force-pushed the bokelley/feat-create-oauth-passthrough-resolver branch from 1910046 to a260021 Compare May 3, 2026 13:40
@bokelley bokelley merged commit defa022 into main May 3, 2026
14 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.

1 participant