v0.8.1
v0.8.1 — Preserve zero values in discover_keywords output
A small, targeted patch fixing one data-fidelity bug introduced in v0.8.0's gRPC → REST migration of discover_keywords.
The bug
discover_keywords used falsy checks (int(v) if v else None) to normalise REST int64 fields. The pattern silently mapped a legitimate 0 to None and lost real data — most visibly on the bid micros bounds:
low_bid_int = int(low_bid_micros) if low_bid_micros else None
...
"low_top_of_page_bid": (
round(low_bid_int / 1_000_000, 2) if low_bid_int else None
),REST sends int64 fields as JSON strings per the proto3 JSON spec, so the string "0" is truthy and survives the first conversion. But once parsed to int 0, it's falsy and the second step swaps it for None. The result: a keyword with lowTopOfPageBidMicros = "0" (Google's representation when no bid data is available, or when the low bound of a competitive range is genuinely 0) came out the other end as low_top_of_page_bid = None — indistinguishable from "field absent". Users couldn't tell missing data apart from "Google has data and the value is 0".
The same defect was present on the high bid bound, and the same fragile first-step pattern lived on avgMonthlySearches and competitionIndex — those happened to round-trip correctly today because their intermediate _int locals are stored directly into the output dict without a second falsy gate, but any future code that re-used them would inherit the bug.
The fix
Two small helpers replace the inline conversions, both using explicit is not None rather than falsy checks:
_maybe_int(value)— proto3-JSON int64 parser. Treats onlyNoneand empty string as "missing"; preserves0exactly; falls back toNoneon malformed input rather than crashing the whole response._micros_to_currency(micros)— micros → 2-dp float. Preserves0(becomes0.0, notNone) andNone.
The output dict now reads:
ideas.append({
"keyword": idea.get("text", ""),
"avg_monthly_searches": _maybe_int(metrics.get("avgMonthlySearches")),
"competition": competition,
"competition_index": _maybe_int(metrics.get("competitionIndex")),
"low_top_of_page_bid": _micros_to_currency(
_maybe_int(metrics.get("lowTopOfPageBidMicros"))
),
"high_top_of_page_bid": _micros_to_currency(
_maybe_int(metrics.get("highTopOfPageBidMicros"))
),
})This pattern can't regress to the original bug by accident — the helpers are the only path that constructs the output, and both are is not None.
Action for maintainers
estimate_budget (same module, gRPC path) has the same falsy-check pattern on clicks / impressions / cost_micros / avg_cpc_micros / total_cost. A legitimate clicks=0 or cost_micros=0 would collapse to None there for the same reason. This is pre-existing behaviour, not regressed by v0.8.0, so this patch is intentionally scoped to discover_keywords (the reported bug). File a separate issue if you want the same defensive treatment for estimate_budget.
Verification
- 258 tests pass (243 v0.8.0 baseline + 15 new in this release)
- New coverage:
TestZeroValuePreservation— end-to-end checks that low_bid=0, high_bid=0, competition_index=0, and avg_monthly=0 all survive the round-tripTestZeroValuePreservation::test_missing_fields_still_return_none— guard against over-correctionTestMaybeIntHelper— unit coverage including the specific wire format"0"regressionTestMicrosToCurrency— unit coverage including_micros_to_currency(0) == 0.0
Install / upgrade
pipx upgrade adloop
# or
pip install --upgrade adloop
# or
uvx adloop@0.8.1
Restart your MCP host after upgrading.
Full Changelog: v0.8.0...v0.8.1