Skip to content

feat(ads): dynamic Google Ads enum introspection helper#33

Open
illia-sapryga wants to merge 2 commits intokLOsk:mainfrom
illia-sapryga:feat/dynamic-ads-enums
Open

feat(ads): dynamic Google Ads enum introspection helper#33
illia-sapryga wants to merge 2 commits intokLOsk:mainfrom
illia-sapryga:feat/dynamic-ads-enums

Conversation

@illia-sapryga
Copy link
Copy Markdown
Contributor

Summary

Adds adloop.ads.enums.enum_names() — a tiny helper that pulls valid enum member names straight from the google-ads SDK at the API version we're pinned to.

The motivation: AdLoop currently hand-maintains parallel lists like

_VALID_CONVERSION_ACTION_TYPES = {"AD_CALL", "WEBSITE_CALL", ...}

inside ads/write.py and elsewhere. Those lists drift the moment the SDK or API version updates — the SDK adds new enum members, our hardcoded set doesn't. Result: AdLoop's validators reject API-valid input, OR (worse) accept input the API would reject because the deny-list logic was outdated.

This PR introduces a single helper that callers use like:

from adloop.ads.enums import enum_names
_VALID_TYPES = enum_names("ConversionActionTypeEnum")

The result is a frozenset of enum member names (e.g. {"AD_CALL", "WEBSITE_CALL", ...}) with the protobuf UNSPECIFIED and UNKNOWN sentinels dropped by default. The helper is functools.lru_cache'd so the no-auth GoogleAdsClient used for introspection is constructed once per process — every enum_names() call after the first is essentially free.

Why no other modules are touched in this PR

This PR is intentionally scoped to just the helper. Downstream PRs (conversion-action management, in-place asset updates, promotion helper refactors) will switch their hardcoded enum sets to enum_names() calls in their own commits.

What this catches

During development, swapping _VALID_CONVERSION_ACTION_TYPES to enum_names("ConversionActionTypeEnum") revealed that the hardcoded list was missing 29 of 40 valid types in the SDK — including CLICK_TO_CALL, FIREBASE_*, ANDROID_APP_PRE_REGISTRATION, etc. AdLoop would have rejected legitimate user input for any of those.

Test plan

  • 12 unit tests covering the helper itself
  • enum_names() returns a frozenset
  • UNSPECIFIED + UNKNOWN are excluded by default
  • exclude_unspecified=False opt-in includes the sentinels
  • Critical members (AD_CALL, WEBSITE_CALL, GOOGLE_SEARCH_ATTRIBUTION_DATA_DRIVEN, USE_RESOURCE_LEVEL_CALL_CONVERSION_ACTION, BLACK_FRIDAY) verified present
  • ConversionActionCountingTypeEnum returns exactly {ONE_PER_CLICK, MANY_PER_CLICK}
  • Unknown enum names raise AttributeError
  • LRU cache returns the same frozenset instance on repeat calls
  • The introspection client is memoized
  • uv run pytest → 196/196 tests pass (184 baseline + 12 new)

Adds adloop.ads.enums.enum_names() — pulls valid enum member names
straight from the google-ads SDK at the API version we're pinned to
(see adloop.ads.client.GOOGLE_ADS_API_VERSION).

Drops the need to hand-maintain parallel lists like:

    _VALID_CONVERSION_ACTION_TYPES = {"AD_CALL", "WEBSITE_CALL", ...}

— which otherwise drift every time the SDK or API version updates.
The helper is module-cached (functools.lru_cache) so the no-auth
GoogleAdsClient used for introspection is built once per process,
and every enum_names() call after the first is essentially free.

UNSPECIFIED + UNKNOWN sentinels are dropped by default since they
are protobuf zero-values that should never appear in user input.

Pure addition. No existing validators changed in this PR; downstream
PRs (conversion-action tools, in-place asset updates, promotion
helper refactors) will switch their hardcoded enum sets to enum_names()
calls in their own commits.

12 unit tests cover:
- enum_names returns a frozenset
- UNSPECIFIED + UNKNOWN are excluded by default and includable on opt-in
- Critical members (AD_CALL, WEBSITE_CALL, GOOGLE_SEARCH_ATTRIBUTION_DATA_DRIVEN,
  USE_RESOURCE_LEVEL_CALL_CONVERSION_ACTION, BLACK_FRIDAY) are present
- ConversionActionCountingTypeEnum returns exactly {ONE_PER_CLICK, MANY_PER_CLICK}
- Unknown enum names raise AttributeError
- LRU cache returns the same frozenset instance on repeat calls
- The introspection client is memoized
The TestModulesUseDynamicEnums class was importing adloop.ads.conversion_actions
and asserting on adloop.ads.write._VALID_PROMOTION_OCCASIONS — modules /
constants that don't exist in this PR. They live in the follow-up PR
(kLOsk#34, feat/asset-and-conversion-tools) and should be tested there.

Branch A is intentionally minimal: the helper + helper-only unit tests.

CI now passes — 12/12 tests.
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