adloop 0.8.0__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {adloop-0.8.0 → adloop-0.8.1}/PKG-INFO +1 -1
  2. {adloop-0.8.0 → adloop-0.8.1}/pyproject.toml +1 -1
  3. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/__init__.py +1 -1
  4. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/forecast.py +44 -18
  5. {adloop-0.8.0 → adloop-0.8.1}/README.md +0 -0
  6. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/__main__.py +0 -0
  7. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/_mcp_patches.py +0 -0
  8. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/__init__.py +0 -0
  9. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/client.py +0 -0
  10. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/currency.py +0 -0
  11. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/enums.py +0 -0
  12. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/gaql.py +0 -0
  13. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/pmax.py +0 -0
  14. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/read.py +0 -0
  15. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/write.py +0 -0
  16. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/auth.py +0 -0
  17. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/bundled_credentials.json +0 -0
  18. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/cli.py +0 -0
  19. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/config.py +0 -0
  20. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/crossref.py +0 -0
  21. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/diagnostics.py +0 -0
  22. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/__init__.py +0 -0
  23. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/client.py +0 -0
  24. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/reports.py +0 -0
  25. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/tracking.py +0 -0
  26. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/__init__.py +0 -0
  27. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/adloop.md +0 -0
  28. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
  29. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/budget-plan.md +0 -0
  30. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/create-ad.md +0 -0
  31. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/create-campaign.md +0 -0
  32. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  33. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  34. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules_install.py +0 -0
  35. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/__init__.py +0 -0
  36. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/audit.py +0 -0
  37. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/guards.py +0 -0
  38. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/preview.py +0 -0
  39. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/server.py +0 -0
  40. {adloop-0.8.0 → adloop-0.8.1}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.
5
5
  Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.8.0"
3
+ version = "0.8.1"
4
4
  description = "Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2,7 +2,7 @@
2
2
 
3
3
  import sys
4
4
 
5
- __version__ = "0.8.0"
5
+ __version__ = "0.8.1"
6
6
 
7
7
 
8
8
  def main() -> None:
@@ -164,6 +164,40 @@ _KEYWORD_IDEAS_REST_URL = (
164
164
  )
165
165
 
166
166
 
167
+ def _maybe_int(value: object) -> int | None:
168
+ """Parse a JSON int64 field into an int, preserving legitimate 0.
169
+
170
+ REST returns ``int64`` fields as JSON strings per the proto3 JSON spec
171
+ (e.g. ``"avgMonthlySearches": "0"``). Falsy checks like
172
+ ``int(v) if v else None`` would map a real ``0`` to ``None`` and
173
+ silently lose data — e.g. a niche keyword with no recorded competition
174
+ or a bid range whose low bound is 0 would disappear from the output.
175
+
176
+ Treat only ``None`` and empty string as "missing"; everything that
177
+ parses cleanly as an int (including ``"0"`` and ``0``) is preserved
178
+ exactly. Anything else falls back to ``None`` rather than crashing
179
+ the whole response.
180
+ """
181
+ if value is None or value == "":
182
+ return None
183
+ try:
184
+ return int(value)
185
+ except (TypeError, ValueError):
186
+ return None
187
+
188
+
189
+ def _micros_to_currency(micros: int | None) -> float | None:
190
+ """Convert micros to a 2-dp currency float, preserving ``0`` and ``None``.
191
+
192
+ A bid bound of 0 micros is meaningful (Google can return 0 for the low
193
+ end of a competitive range or when no bid data is available for a
194
+ keyword) and must not be collapsed to ``None``.
195
+ """
196
+ if micros is None:
197
+ return None
198
+ return round(micros / 1_000_000, 2)
199
+
200
+
167
201
  def _build_keyword_ideas_rest_body(
168
202
  *,
169
203
  language_id: str,
@@ -300,30 +334,22 @@ def discover_keywords(
300
334
 
301
335
  for idea in payload.get("results", []):
302
336
  metrics = idea.get("keywordIdeaMetrics", {}) or {}
303
- avg_monthly = metrics.get("avgMonthlySearches")
304
337
  competition = metrics.get("competition") or "UNSPECIFIED"
305
- competition_index = metrics.get("competitionIndex")
306
- low_bid_micros = metrics.get("lowTopOfPageBidMicros")
307
- high_bid_micros = metrics.get("highTopOfPageBidMicros")
308
-
309
- # int64 fields come back as JSON strings in REST — normalize.
310
- avg_monthly_int = int(avg_monthly) if avg_monthly else None
311
- competition_index_int = (
312
- int(competition_index) if competition_index else None
313
- )
314
- low_bid_int = int(low_bid_micros) if low_bid_micros else None
315
- high_bid_int = int(high_bid_micros) if high_bid_micros else None
316
338
 
339
+ # REST returns int64 fields as JSON strings; ``_maybe_int`` and
340
+ # ``_micros_to_currency`` preserve legitimate 0 values that
341
+ # falsy checks would otherwise silently drop. See the helper
342
+ # docstrings for the failure modes this defends against.
317
343
  ideas.append({
318
344
  "keyword": idea.get("text", ""),
319
- "avg_monthly_searches": avg_monthly_int,
345
+ "avg_monthly_searches": _maybe_int(metrics.get("avgMonthlySearches")),
320
346
  "competition": competition,
321
- "competition_index": competition_index_int,
322
- "low_top_of_page_bid": (
323
- round(low_bid_int / 1_000_000, 2) if low_bid_int else None
347
+ "competition_index": _maybe_int(metrics.get("competitionIndex")),
348
+ "low_top_of_page_bid": _micros_to_currency(
349
+ _maybe_int(metrics.get("lowTopOfPageBidMicros"))
324
350
  ),
325
- "high_top_of_page_bid": (
326
- round(high_bid_int / 1_000_000, 2) if high_bid_int else None
351
+ "high_top_of_page_bid": _micros_to_currency(
352
+ _maybe_int(metrics.get("highTopOfPageBidMicros"))
327
353
  ),
328
354
  })
329
355
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes