adloop 0.27.0__tar.gz → 0.27.2__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.27.0 → adloop-0.27.2}/PKG-INFO +6 -5
- {adloop-0.27.0 → adloop-0.27.2}/README.md +5 -4
- {adloop-0.27.0 → adloop-0.27.2}/pyproject.toml +1 -1
- {adloop-0.27.0 → adloop-0.27.2}/pyproject.toml.orig +1 -1
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/write.py +32 -4
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/cli.py +136 -58
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/server.py +10 -5
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/__main__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/brands.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/currency.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/enums.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/read.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/shopping.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/auth.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/config.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/crossref.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/diagnostics.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ga4/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/ga4/write.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gsc/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gtm/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gtm/read.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/gtm/write.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/merchant/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/merchant/read.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/pagespeed.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/client.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/read.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/reddit/write.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/adloop.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/rules_install.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/runtime.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/approval.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/approval_card.html +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/audit.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/guards.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/safety/preview.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/tracking.py +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/upload_card.html +0 -0
- {adloop-0.27.0 → adloop-0.27.2}/src/adloop/uploads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: adloop
|
|
3
|
-
Version: 0.27.
|
|
3
|
+
Version: 0.27.2
|
|
4
4
|
Summary: The AI command center for Google Ads, Reddit Ads, GA4, and tracking code.
|
|
5
5
|
Keywords: mcp,google-ads,reddit-ads,google-analytics,ga4,cursor,marketing
|
|
6
6
|
Author: Daniel Klose
|
|
@@ -404,7 +404,7 @@ uv run adloop init
|
|
|
404
404
|
|
|
405
405
|
### What `adloop init` does
|
|
406
406
|
|
|
407
|
-
The wizard walks you through:
|
|
407
|
+
The wizard first asks whether to set up Google. Answer no to run AdLoop on Reddit Ads alone: it skips steps 1 to 6 and goes straight to the Reddit step. Otherwise it walks you through:
|
|
408
408
|
|
|
409
409
|
1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
|
|
410
410
|
2. **Google Ads API access** — applied for on your Cloud project's [Google Ads API Overview page](https://console.cloud.google.com/google/ads-apis/overview); the legacy developer-token prompt can be left empty
|
|
@@ -412,9 +412,10 @@ The wizard walks you through:
|
|
|
412
412
|
4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
|
|
413
413
|
5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
|
|
414
414
|
6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
|
|
415
|
-
7. **
|
|
416
|
-
8. **
|
|
417
|
-
9. **
|
|
415
|
+
7. **Reddit Ads** — optional next to Google, required without it: your own Reddit developer app, sign-in, and the default ad account
|
|
416
|
+
8. **Safety defaults** — budget cap and dry-run preference
|
|
417
|
+
9. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
|
|
418
|
+
10. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
|
|
418
419
|
|
|
419
420
|
### Requirements
|
|
420
421
|
|
|
@@ -376,7 +376,7 @@ uv run adloop init
|
|
|
376
376
|
|
|
377
377
|
### What `adloop init` does
|
|
378
378
|
|
|
379
|
-
The wizard walks you through:
|
|
379
|
+
The wizard first asks whether to set up Google. Answer no to run AdLoop on Reddit Ads alone: it skips steps 1 to 6 and goes straight to the Reddit step. Otherwise it walks you through:
|
|
380
380
|
|
|
381
381
|
1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
|
|
382
382
|
2. **Google Ads API access** — applied for on your Cloud project's [Google Ads API Overview page](https://console.cloud.google.com/google/ads-apis/overview); the legacy developer-token prompt can be left empty
|
|
@@ -384,9 +384,10 @@ The wizard walks you through:
|
|
|
384
384
|
4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
|
|
385
385
|
5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
|
|
386
386
|
6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
|
|
387
|
-
7. **
|
|
388
|
-
8. **
|
|
389
|
-
9. **
|
|
387
|
+
7. **Reddit Ads** — optional next to Google, required without it: your own Reddit developer app, sign-in, and the default ad account
|
|
388
|
+
8. **Safety defaults** — budget cap and dry-run preference
|
|
389
|
+
9. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
|
|
390
|
+
10. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
|
|
390
391
|
|
|
391
392
|
### Requirements
|
|
392
393
|
|
|
@@ -7,6 +7,7 @@ Every write tool returns a preview/plan. Nothing executes until
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import hashlib
|
|
10
|
+
import re
|
|
10
11
|
import struct
|
|
11
12
|
from pathlib import Path, PurePath, PurePosixPath
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
@@ -40,6 +41,31 @@ _VALID_IMAGE_MIME_TYPES = {
|
|
|
40
41
|
_VALID_HEADLINE_PINS = {"HEADLINE_1", "HEADLINE_2", "HEADLINE_3"}
|
|
41
42
|
_VALID_DESCRIPTION_PINS = {"DESCRIPTION_1", "DESCRIPTION_2"}
|
|
42
43
|
|
|
44
|
+
# Insertion tags Google expands at serve time. Only the default text after
|
|
45
|
+
# the colon counts toward the 30/90 character caps, e.g.
|
|
46
|
+
# "{KeyWord:Clogged Drain Service}" is 21 characters, not 31. Written with
|
|
47
|
+
# spaces ("{ KeyWord : x }") it is not a tag and counts as plain text.
|
|
48
|
+
_INSERTION_TAG_RE = re.compile(r"\{(keyword|location\([a-z]+\)):([^{}]*)\}", re.IGNORECASE)
|
|
49
|
+
_BARE_INSERTION_TAG_RE = re.compile(r"\{(keyword|location\([a-z]+\))\}", re.IGNORECASE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _counted_length(text: str) -> int:
|
|
53
|
+
"""Length Google checks against the cap: insertion tags count as their default text."""
|
|
54
|
+
return len(_INSERTION_TAG_RE.sub(lambda m: m.group(2), text))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _insertion_tag_errors(label: str, text: str) -> list[str]:
|
|
58
|
+
"""RSA keyword/location insertion needs default text, e.g. {KeyWord:Drain Cleaning}."""
|
|
59
|
+
missing = _BARE_INSERTION_TAG_RE.search(text) or any(
|
|
60
|
+
not m.group(2).strip() for m in _INSERTION_TAG_RE.finditer(text)
|
|
61
|
+
)
|
|
62
|
+
if missing:
|
|
63
|
+
return [
|
|
64
|
+
f"{label} uses an insertion tag without default text: '{text}'. "
|
|
65
|
+
"Default text after a colon is required, e.g. {KeyWord:Drain Cleaning}"
|
|
66
|
+
]
|
|
67
|
+
return []
|
|
68
|
+
|
|
43
69
|
|
|
44
70
|
def _normalize_rsa_assets(items: list) -> list[dict]:
|
|
45
71
|
"""Accept str or {text, pinned_field?} dict entries; return list of dicts.
|
|
@@ -3435,9 +3461,10 @@ def _validate_rsa_assets(
|
|
|
3435
3461
|
for i, h in enumerate(headlines):
|
|
3436
3462
|
text = h["text"]
|
|
3437
3463
|
pin = h["pinned_field"]
|
|
3438
|
-
|
|
3464
|
+
errors.extend(_insertion_tag_errors(f"Headline {i + 1}", text))
|
|
3465
|
+
if _counted_length(text) > 30:
|
|
3439
3466
|
errors.append(
|
|
3440
|
-
f"Headline {i + 1} exceeds 30 chars ({
|
|
3467
|
+
f"Headline {i + 1} exceeds 30 chars ({_counted_length(text)}): '{text}'"
|
|
3441
3468
|
)
|
|
3442
3469
|
if pin is not None:
|
|
3443
3470
|
if pin not in _VALID_HEADLINE_PINS:
|
|
@@ -3455,9 +3482,10 @@ def _validate_rsa_assets(
|
|
|
3455
3482
|
for i, d in enumerate(descriptions):
|
|
3456
3483
|
text = d["text"]
|
|
3457
3484
|
pin = d["pinned_field"]
|
|
3458
|
-
|
|
3485
|
+
errors.extend(_insertion_tag_errors(f"Description {i + 1}", text))
|
|
3486
|
+
if _counted_length(text) > 90:
|
|
3459
3487
|
errors.append(
|
|
3460
|
-
f"Description {i + 1} exceeds 90 chars ({
|
|
3488
|
+
f"Description {i + 1} exceeds 90 chars ({_counted_length(text)}): '{text}'"
|
|
3461
3489
|
)
|
|
3462
3490
|
if pin is not None:
|
|
3463
3491
|
if pin not in _VALID_DESCRIPTION_PINS:
|
|
@@ -414,7 +414,7 @@ def _wizard_gsc_step(oauth_ok: bool, _existing) -> str:
|
|
|
414
414
|
)
|
|
415
415
|
|
|
416
416
|
|
|
417
|
-
def _wizard_reddit_step(_existing) -> dict[str, str]:
|
|
417
|
+
def _wizard_reddit_step(_existing, required: bool = False) -> dict[str, str]:
|
|
418
418
|
"""Optionally connect Reddit Ads: own developer app, own OAuth, own token file.
|
|
419
419
|
|
|
420
420
|
Returns the ``reddit:`` values for the config. Reddit's redirect URI
|
|
@@ -430,7 +430,8 @@ def _wizard_reddit_step(_existing) -> dict[str, str]:
|
|
|
430
430
|
"ad_account_id": _existing("reddit", "ad_account_id"),
|
|
431
431
|
"username": _existing("reddit", "username"),
|
|
432
432
|
}
|
|
433
|
-
|
|
433
|
+
# Without Google, Reddit is the platform being set up: no opt-out.
|
|
434
|
+
if not required and not _prompt_bool("Connect Reddit Ads? (optional)", default=bool(existing_id)):
|
|
434
435
|
return values
|
|
435
436
|
|
|
436
437
|
_print()
|
|
@@ -589,6 +590,70 @@ def run_init_wizard() -> None:
|
|
|
589
590
|
return str(existing_config[section].get(key, fallback))
|
|
590
591
|
return fallback
|
|
591
592
|
|
|
593
|
+
# Both platforms are optional; a workspace can run on Reddit alone.
|
|
594
|
+
_print(" AdLoop connects Google (Ads, Analytics, Tag Manager, Search Console)")
|
|
595
|
+
_print(" and Reddit Ads. Set up one or both.")
|
|
596
|
+
_print()
|
|
597
|
+
use_google = _prompt_bool(
|
|
598
|
+
"Set up Google?", default=_existing("google", "credentials_path") != "" or existing_config is None
|
|
599
|
+
)
|
|
600
|
+
credentials_path = project_id = developer_token = login_customer_id = ""
|
|
601
|
+
step_num = 0
|
|
602
|
+
if use_google:
|
|
603
|
+
step_num = _wizard_google_steps(_existing)
|
|
604
|
+
credentials_path, project_id, developer_token, login_customer_id = _WIZARD_GOOGLE
|
|
605
|
+
|
|
606
|
+
# OAuth + auto-discovery
|
|
607
|
+
step_num += 1
|
|
608
|
+
_step_header(step_num, "Authorization & Account Discovery" if use_google else "Writing a first config")
|
|
609
|
+
|
|
610
|
+
# Write a temporary config for OAuth + discovery. If the wizard is
|
|
611
|
+
# interrupted after this point, _cleanup_on_cancel restores the original.
|
|
612
|
+
_ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
|
|
613
|
+
temp_config_yaml = _generate_config_yaml(
|
|
614
|
+
project_id=project_id,
|
|
615
|
+
credentials_path=credentials_path,
|
|
616
|
+
property_id="",
|
|
617
|
+
developer_token=developer_token,
|
|
618
|
+
customer_id="",
|
|
619
|
+
login_customer_id=login_customer_id,
|
|
620
|
+
max_daily_budget=50.0,
|
|
621
|
+
require_dry_run=True,
|
|
622
|
+
)
|
|
623
|
+
_CONFIG_PATH.write_text(temp_config_yaml)
|
|
624
|
+
|
|
625
|
+
# Everything below uses the temp config for OAuth and discovery.
|
|
626
|
+
# If the wizard is interrupted, restore the original config (or remove
|
|
627
|
+
# the temp) so we never leave a half-baked config on disk.
|
|
628
|
+
try:
|
|
629
|
+
_run_wizard_post_config(
|
|
630
|
+
credentials_path=credentials_path,
|
|
631
|
+
project_id=project_id,
|
|
632
|
+
developer_token=developer_token,
|
|
633
|
+
login_customer_id=login_customer_id,
|
|
634
|
+
step_num=step_num,
|
|
635
|
+
_existing=_existing,
|
|
636
|
+
use_google=use_google,
|
|
637
|
+
)
|
|
638
|
+
except KeyboardInterrupt:
|
|
639
|
+
if _original_config_backup is not None:
|
|
640
|
+
_CONFIG_PATH.write_text(_original_config_backup)
|
|
641
|
+
elif _CONFIG_PATH.exists():
|
|
642
|
+
_CONFIG_PATH.unlink()
|
|
643
|
+
raise
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
_WIZARD_GOOGLE: tuple[str, str, str, str] = ("", "", "", "")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _wizard_google_steps(_existing) -> int:
|
|
650
|
+
"""The Google project, credentials and API access steps; returns the last step number.
|
|
651
|
+
|
|
652
|
+
The collected values land in ``_WIZARD_GOOGLE`` (credentials path,
|
|
653
|
+
project id, developer token, login customer id).
|
|
654
|
+
"""
|
|
655
|
+
global _WIZARD_GOOGLE
|
|
656
|
+
|
|
592
657
|
# Step 1: Google Cloud project
|
|
593
658
|
#
|
|
594
659
|
# AdLoop no longer ships OAuth credentials. Every install brings its
|
|
@@ -641,44 +706,8 @@ def run_init_wizard() -> None:
|
|
|
641
706
|
default=_existing("ads", "login_customer_id"),
|
|
642
707
|
required=False,
|
|
643
708
|
)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
step_num += 1
|
|
647
|
-
_step_header(step_num, "Authorization & Account Discovery")
|
|
648
|
-
|
|
649
|
-
# Write a temporary config for OAuth + discovery. If the wizard is
|
|
650
|
-
# interrupted after this point, _cleanup_on_cancel restores the original.
|
|
651
|
-
_ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
|
|
652
|
-
temp_config_yaml = _generate_config_yaml(
|
|
653
|
-
project_id=project_id,
|
|
654
|
-
credentials_path=credentials_path,
|
|
655
|
-
property_id="",
|
|
656
|
-
developer_token=developer_token,
|
|
657
|
-
customer_id="",
|
|
658
|
-
login_customer_id=login_customer_id,
|
|
659
|
-
max_daily_budget=50.0,
|
|
660
|
-
require_dry_run=True,
|
|
661
|
-
)
|
|
662
|
-
_CONFIG_PATH.write_text(temp_config_yaml)
|
|
663
|
-
|
|
664
|
-
# Everything below uses the temp config for OAuth and discovery.
|
|
665
|
-
# If the wizard is interrupted, restore the original config (or remove
|
|
666
|
-
# the temp) so we never leave a half-baked config on disk.
|
|
667
|
-
try:
|
|
668
|
-
_run_wizard_post_config(
|
|
669
|
-
credentials_path=credentials_path,
|
|
670
|
-
project_id=project_id,
|
|
671
|
-
developer_token=developer_token,
|
|
672
|
-
login_customer_id=login_customer_id,
|
|
673
|
-
step_num=step_num,
|
|
674
|
-
_existing=_existing,
|
|
675
|
-
)
|
|
676
|
-
except KeyboardInterrupt:
|
|
677
|
-
if _original_config_backup is not None:
|
|
678
|
-
_CONFIG_PATH.write_text(_original_config_backup)
|
|
679
|
-
elif _CONFIG_PATH.exists():
|
|
680
|
-
_CONFIG_PATH.unlink()
|
|
681
|
-
raise
|
|
709
|
+
_WIZARD_GOOGLE = (credentials_path, project_id, developer_token, login_customer_id)
|
|
710
|
+
return step_num
|
|
682
711
|
|
|
683
712
|
|
|
684
713
|
def _run_wizard_post_config(
|
|
@@ -689,10 +718,9 @@ def _run_wizard_post_config(
|
|
|
689
718
|
login_customer_id: str,
|
|
690
719
|
step_num: int,
|
|
691
720
|
_existing: object,
|
|
721
|
+
use_google: bool = True,
|
|
692
722
|
) -> None:
|
|
693
723
|
"""Run the wizard steps after the temp config has been written."""
|
|
694
|
-
from adloop.config import load_config
|
|
695
|
-
|
|
696
724
|
# Optional: copy custom credentials to ~/.adloop/
|
|
697
725
|
if credentials_path:
|
|
698
726
|
creds_expanded = Path(credentials_path).expanduser()
|
|
@@ -706,6 +734,54 @@ def _run_wizard_post_config(
|
|
|
706
734
|
shutil.copy2(creds_expanded, adloop_creds)
|
|
707
735
|
_print(f" ✓ Credentials copied to {adloop_creds}")
|
|
708
736
|
|
|
737
|
+
oauth_ok = False
|
|
738
|
+
property_id = ""
|
|
739
|
+
customer_id = ""
|
|
740
|
+
gtm_account_id = gtm_container_id = gsc_site_url = ""
|
|
741
|
+
if use_google:
|
|
742
|
+
oauth_ok, property_id, customer_id, step_num = _wizard_google_discovery(step_num, _existing)
|
|
743
|
+
|
|
744
|
+
# Optional service pins: GTM / Search Console / PageSpeed. All three
|
|
745
|
+
# work without config (explicit args or discovery per call) — a pin
|
|
746
|
+
# just saves the model a lookup and locks tools to the right property.
|
|
747
|
+
step_num += 1
|
|
748
|
+
if use_google:
|
|
749
|
+
_step_header(step_num, "Optional Services (GTM, Search Console, PageSpeed)")
|
|
750
|
+
gtm_account_id, gtm_container_id = _wizard_gtm_step(oauth_ok, _existing)
|
|
751
|
+
gsc_site_url = _wizard_gsc_step(oauth_ok, _existing)
|
|
752
|
+
else:
|
|
753
|
+
_step_header(step_num, "PageSpeed (optional)")
|
|
754
|
+
pagespeed_api_key = _prompt(
|
|
755
|
+
"PageSpeed Insights API key (optional, Enter to skip)",
|
|
756
|
+
default=_existing("pagespeed", "api_key"), # type: ignore[operator]
|
|
757
|
+
required=False,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Reddit Ads: a second ad platform with its own OAuth app and token file.
|
|
761
|
+
step_num += 1
|
|
762
|
+
_step_header(step_num, "Reddit Ads" if not use_google else "Reddit Ads (optional)")
|
|
763
|
+
reddit_values = _wizard_reddit_step(_existing, required=not use_google)
|
|
764
|
+
_wizard_finish(
|
|
765
|
+
step_num=step_num,
|
|
766
|
+
_existing=_existing,
|
|
767
|
+
credentials_path=credentials_path,
|
|
768
|
+
project_id=project_id,
|
|
769
|
+
developer_token=developer_token,
|
|
770
|
+
login_customer_id=login_customer_id,
|
|
771
|
+
property_id=property_id,
|
|
772
|
+
customer_id=customer_id,
|
|
773
|
+
gtm_account_id=gtm_account_id,
|
|
774
|
+
gtm_container_id=gtm_container_id,
|
|
775
|
+
gsc_site_url=gsc_site_url,
|
|
776
|
+
pagespeed_api_key=pagespeed_api_key,
|
|
777
|
+
reddit_values=reddit_values,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _wizard_google_discovery(step_num: int, _existing) -> tuple[bool, str, str, int]:
|
|
782
|
+
"""Google sign-in plus GA4 and Ads account discovery: (oauth_ok, property_id, customer_id, step)."""
|
|
783
|
+
from adloop.config import load_config
|
|
784
|
+
|
|
709
785
|
_print(" Signing in with Google (this may open a browser)...")
|
|
710
786
|
_print()
|
|
711
787
|
oauth_ok = False
|
|
@@ -797,24 +873,26 @@ def _run_wizard_post_config(
|
|
|
797
873
|
default=_existing("ads", "customer_id"), # type: ignore[operator]
|
|
798
874
|
)
|
|
799
875
|
|
|
800
|
-
|
|
801
|
-
# work without config (explicit args or discovery per call) — a pin
|
|
802
|
-
# just saves the model a lookup and locks tools to the right property.
|
|
803
|
-
step_num += 1
|
|
804
|
-
_step_header(step_num, "Optional Services (GTM, Search Console, PageSpeed)")
|
|
805
|
-
gtm_account_id, gtm_container_id = _wizard_gtm_step(oauth_ok, _existing)
|
|
806
|
-
gsc_site_url = _wizard_gsc_step(oauth_ok, _existing)
|
|
807
|
-
pagespeed_api_key = _prompt(
|
|
808
|
-
"PageSpeed Insights API key (optional, Enter to skip)",
|
|
809
|
-
default=_existing("pagespeed", "api_key"), # type: ignore[operator]
|
|
810
|
-
required=False,
|
|
811
|
-
)
|
|
876
|
+
return oauth_ok, property_id, customer_id, step_num
|
|
812
877
|
|
|
813
|
-
# Reddit Ads: a second ad platform with its own OAuth app and token file.
|
|
814
|
-
step_num += 1
|
|
815
|
-
_step_header(step_num, "Reddit Ads (optional)")
|
|
816
|
-
reddit_values = _wizard_reddit_step(_existing)
|
|
817
878
|
|
|
879
|
+
def _wizard_finish(
|
|
880
|
+
*,
|
|
881
|
+
step_num: int,
|
|
882
|
+
_existing,
|
|
883
|
+
credentials_path: str,
|
|
884
|
+
project_id: str,
|
|
885
|
+
developer_token: str,
|
|
886
|
+
login_customer_id: str,
|
|
887
|
+
property_id: str,
|
|
888
|
+
customer_id: str,
|
|
889
|
+
gtm_account_id: str,
|
|
890
|
+
gtm_container_id: str,
|
|
891
|
+
gsc_site_url: str,
|
|
892
|
+
pagespeed_api_key: str,
|
|
893
|
+
reddit_values: dict,
|
|
894
|
+
) -> None:
|
|
895
|
+
"""Safety defaults, the final config and the client snippets."""
|
|
818
896
|
# Safety defaults
|
|
819
897
|
step_num += 1
|
|
820
898
|
_step_header(step_num, "Safety Defaults")
|
|
@@ -3117,10 +3117,13 @@ def draft_responsive_search_ad(
|
|
|
3117
3117
|
ad_group_id: Numeric ID of the ad group that gets the ad.
|
|
3118
3118
|
headlines: 3-15 headlines, max 30 chars each. Each entry is a plain
|
|
3119
3119
|
string (unpinned) or {"text": "...", "pinned_field": "HEADLINE_1"};
|
|
3120
|
-
valid pins are HEADLINE_1, HEADLINE_2, HEADLINE_3.
|
|
3120
|
+
valid pins are HEADLINE_1, HEADLINE_2, HEADLINE_3. Keyword or
|
|
3121
|
+
location insertion such as {KeyWord:Drain Cleaning} counts as its
|
|
3122
|
+
default text, which is required.
|
|
3121
3123
|
descriptions: 2-4 descriptions, max 90 chars each. Each entry is a plain
|
|
3122
3124
|
string (unpinned) or {"text": "...", "pinned_field": "DESCRIPTION_1"};
|
|
3123
|
-
valid pins are DESCRIPTION_1, DESCRIPTION_2.
|
|
3125
|
+
valid pins are DESCRIPTION_1, DESCRIPTION_2. Insertion tags count
|
|
3126
|
+
as their default text, as for headlines.
|
|
3124
3127
|
final_url: Landing page URL the ad links to; must be reachable.
|
|
3125
3128
|
customer_id: Google Ads customer ID (digits, e.g. "1234567890"). Empty
|
|
3126
3129
|
uses the configured default account.
|
|
@@ -3183,11 +3186,13 @@ def update_responsive_search_ad(
|
|
|
3183
3186
|
fully swaps in for the existing headlines. Google's RSA constraints
|
|
3184
3187
|
apply: 3-15 headlines, max 30 chars each, at most 2 per pin slot.
|
|
3185
3188
|
Each entry is a plain string (unpinned) or {"text": "...",
|
|
3186
|
-
"pinned_field": "HEADLINE_1"} (HEADLINE_1-3).
|
|
3189
|
+
"pinned_field": "HEADLINE_1"} (HEADLINE_1-3). Insertion tags such as
|
|
3190
|
+
{KeyWord:Clogged Drain Service} count as their default text, which
|
|
3191
|
+
is required.
|
|
3187
3192
|
descriptions: LIST-REPLACE: None or [] means no change; a non-empty list
|
|
3188
3193
|
fully swaps in. 2-4 descriptions, max 90 chars each, at most 1 per
|
|
3189
|
-
pin slot (DESCRIPTION_1, DESCRIPTION_2). Same entry format
|
|
3190
|
-
headlines.
|
|
3194
|
+
pin slot (DESCRIPTION_1, DESCRIPTION_2). Same entry format and
|
|
3195
|
+
insertion-tag counting as headlines.
|
|
3191
3196
|
final_url: Empty means no change; non-empty replaces the final URL and
|
|
3192
3197
|
is checked for reachability.
|
|
3193
3198
|
path1: Empty means no change; non-empty sets the first display path
|
|
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
|
|
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
|