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.
- {adloop-0.8.0 → adloop-0.8.1}/PKG-INFO +1 -1
- {adloop-0.8.0 → adloop-0.8.1}/pyproject.toml +1 -1
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/__init__.py +1 -1
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/forecast.py +44 -18
- {adloop-0.8.0 → adloop-0.8.1}/README.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/__main__.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/client.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/currency.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/enums.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/read.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ads/write.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/auth.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/bundled_credentials.json +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/cli.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/config.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/crossref.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/diagnostics.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/client.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/adloop.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/rules_install.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/audit.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/guards.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/safety/preview.py +0 -0
- {adloop-0.8.0 → adloop-0.8.1}/src/adloop/server.py +0 -0
- {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.
|
|
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
|
|
@@ -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":
|
|
345
|
+
"avg_monthly_searches": _maybe_int(metrics.get("avgMonthlySearches")),
|
|
320
346
|
"competition": competition,
|
|
321
|
-
"competition_index":
|
|
322
|
-
"low_top_of_page_bid": (
|
|
323
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|