adloop 0.4.1__tar.gz → 0.4.3__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.4.1 → adloop-0.4.3}/PKG-INFO +21 -12
- {adloop-0.4.1 → adloop-0.4.3}/README.md +20 -11
- {adloop-0.4.1 → adloop-0.4.3}/pyproject.toml +1 -1
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/__init__.py +10 -2
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/read.py +5 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/write.py +42 -21
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/cli.py +83 -39
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/server.py +2 -1
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/__main__.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/client.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/currency.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/auth.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/bundled_credentials.json +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/config.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/crossref.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ga4/client.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/safety/audit.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/safety/guards.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/safety/preview.py +0 -0
- {adloop-0.4.1 → adloop-0.4.3}/src/adloop/tracking.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: adloop
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -224,6 +224,25 @@ The wizard:
|
|
|
224
224
|
|
|
225
225
|
- Python 3.11+
|
|
226
226
|
- A Google Ads account with an MCC (Manager Account)
|
|
227
|
+
- A Google Ads Developer Token (see below)
|
|
228
|
+
|
|
229
|
+
### Google Ads Developer Token
|
|
230
|
+
|
|
231
|
+
A developer token is **always required** — even when using AdLoop's built-in OAuth credentials. The built-in credentials handle Google sign-in; the developer token is a separate key that grants API access to your Google Ads data.
|
|
232
|
+
|
|
233
|
+
1. **Create an MCC** (free) at [ads.google.com/home/tools/manager-accounts](https://ads.google.com/home/tools/manager-accounts/) if you don't have one. Link your regular Google Ads account to it.
|
|
234
|
+
2. In the MCC, go to **Tools & Settings → API Center**
|
|
235
|
+
3. Your **developer token** is shown there. Copy it — the wizard will ask for it.
|
|
236
|
+
|
|
237
|
+
**Access levels** — your token's access level determines what it can do:
|
|
238
|
+
|
|
239
|
+
| Level | How to Get | What It Allows |
|
|
240
|
+
|-------|-----------|----------------|
|
|
241
|
+
| **Test Account** | Default for new tokens | Can only access test accounts — **not production accounts**. If you see `DEVELOPER_TOKEN_NOT_APPROVED`, this is why. |
|
|
242
|
+
| **Explorer** | Automatic after first API call with a production account | 2,880 operations/day on production accounts. Enough to get started. |
|
|
243
|
+
| **Basic** | [Apply through API Center](https://ads.google.com/aw/apicenter) | 15,000 operations/day. Apply if you need more. |
|
|
244
|
+
|
|
245
|
+
> **Getting `DEVELOPER_TOKEN_NOT_APPROVED`?** Your token is at "Test Account" level. Go to [API Center](https://ads.google.com/aw/apicenter) in your MCC and check your access level. If it shows "Test Account", you need to apply for Basic access or wait for Explorer access to be granted after your first production API call.
|
|
227
246
|
|
|
228
247
|
### Headless Servers
|
|
229
248
|
|
|
@@ -253,17 +272,7 @@ When you run `adloop init`, choose "No" when asked about built-in credentials. T
|
|
|
253
272
|
|
|
254
273
|
> Service accounts are also supported — just place the service account key JSON at the same `credentials_path`. AdLoop detects the file type automatically.
|
|
255
274
|
|
|
256
|
-
#### Step 3 —
|
|
257
|
-
|
|
258
|
-
1. **Create an MCC** (free) at [ads.google.com/home/tools/manager-accounts](https://ads.google.com/home/tools/manager-accounts/) if you don't have one. Link your regular Google Ads account to it.
|
|
259
|
-
2. In the MCC, go to **Tools & Settings → API Center**
|
|
260
|
-
3. Your **developer token** is shown there. Copy it.
|
|
261
|
-
|
|
262
|
-
Access levels:
|
|
263
|
-
- **Explorer** (automatic) — 2,880 operations/day on production accounts. Enough to get started.
|
|
264
|
-
- **Basic** (requires application) — 15,000 operations/day. Apply through the same API Center page if you need more.
|
|
265
|
-
|
|
266
|
-
#### Step 4 — Connect to Your Editor
|
|
275
|
+
#### Step 3 — Connect to Your Editor
|
|
267
276
|
|
|
268
277
|
**Cursor** — Add to your project's `.cursor/mcp.json`:
|
|
269
278
|
|
|
@@ -200,6 +200,25 @@ The wizard:
|
|
|
200
200
|
|
|
201
201
|
- Python 3.11+
|
|
202
202
|
- A Google Ads account with an MCC (Manager Account)
|
|
203
|
+
- A Google Ads Developer Token (see below)
|
|
204
|
+
|
|
205
|
+
### Google Ads Developer Token
|
|
206
|
+
|
|
207
|
+
A developer token is **always required** — even when using AdLoop's built-in OAuth credentials. The built-in credentials handle Google sign-in; the developer token is a separate key that grants API access to your Google Ads data.
|
|
208
|
+
|
|
209
|
+
1. **Create an MCC** (free) at [ads.google.com/home/tools/manager-accounts](https://ads.google.com/home/tools/manager-accounts/) if you don't have one. Link your regular Google Ads account to it.
|
|
210
|
+
2. In the MCC, go to **Tools & Settings → API Center**
|
|
211
|
+
3. Your **developer token** is shown there. Copy it — the wizard will ask for it.
|
|
212
|
+
|
|
213
|
+
**Access levels** — your token's access level determines what it can do:
|
|
214
|
+
|
|
215
|
+
| Level | How to Get | What It Allows |
|
|
216
|
+
|-------|-----------|----------------|
|
|
217
|
+
| **Test Account** | Default for new tokens | Can only access test accounts — **not production accounts**. If you see `DEVELOPER_TOKEN_NOT_APPROVED`, this is why. |
|
|
218
|
+
| **Explorer** | Automatic after first API call with a production account | 2,880 operations/day on production accounts. Enough to get started. |
|
|
219
|
+
| **Basic** | [Apply through API Center](https://ads.google.com/aw/apicenter) | 15,000 operations/day. Apply if you need more. |
|
|
220
|
+
|
|
221
|
+
> **Getting `DEVELOPER_TOKEN_NOT_APPROVED`?** Your token is at "Test Account" level. Go to [API Center](https://ads.google.com/aw/apicenter) in your MCC and check your access level. If it shows "Test Account", you need to apply for Basic access or wait for Explorer access to be granted after your first production API call.
|
|
203
222
|
|
|
204
223
|
### Headless Servers
|
|
205
224
|
|
|
@@ -229,17 +248,7 @@ When you run `adloop init`, choose "No" when asked about built-in credentials. T
|
|
|
229
248
|
|
|
230
249
|
> Service accounts are also supported — just place the service account key JSON at the same `credentials_path`. AdLoop detects the file type automatically.
|
|
231
250
|
|
|
232
|
-
#### Step 3 —
|
|
233
|
-
|
|
234
|
-
1. **Create an MCC** (free) at [ads.google.com/home/tools/manager-accounts](https://ads.google.com/home/tools/manager-accounts/) if you don't have one. Link your regular Google Ads account to it.
|
|
235
|
-
2. In the MCC, go to **Tools & Settings → API Center**
|
|
236
|
-
3. Your **developer token** is shown there. Copy it.
|
|
237
|
-
|
|
238
|
-
Access levels:
|
|
239
|
-
- **Explorer** (automatic) — 2,880 operations/day on production accounts. Enough to get started.
|
|
240
|
-
- **Basic** (requires application) — 15,000 operations/day. Apply through the same API Center page if you need more.
|
|
241
|
-
|
|
242
|
-
#### Step 4 — Connect to Your Editor
|
|
251
|
+
#### Step 3 — Connect to Your Editor
|
|
243
252
|
|
|
244
253
|
**Cursor** — Add to your project's `.cursor/mcp.json`:
|
|
245
254
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
|
-
__version__ = "0.4.
|
|
5
|
+
__version__ = "0.4.3"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
@@ -11,10 +11,18 @@ def main() -> None:
|
|
|
11
11
|
Routes to the setup wizard when called as ``adloop init``,
|
|
12
12
|
otherwise starts the MCP server.
|
|
13
13
|
"""
|
|
14
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
|
|
15
|
+
print(f"adloop {__version__}")
|
|
16
|
+
return
|
|
17
|
+
|
|
14
18
|
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
|
15
19
|
from adloop.cli import run_init_wizard
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
try:
|
|
22
|
+
run_init_wizard()
|
|
23
|
+
except KeyboardInterrupt:
|
|
24
|
+
print("\n\n Setup cancelled.\n")
|
|
25
|
+
sys.exit(130)
|
|
18
26
|
else:
|
|
19
27
|
from adloop.server import mcp
|
|
20
28
|
|
|
@@ -214,6 +214,11 @@ def get_negative_keywords(
|
|
|
214
214
|
"""
|
|
215
215
|
|
|
216
216
|
rows = execute_query(config, customer_id, query)
|
|
217
|
+
for row in rows:
|
|
218
|
+
cid = row.get("campaign.id")
|
|
219
|
+
crit_id = row.get("campaign_criterion.criterion_id")
|
|
220
|
+
if cid and crit_id:
|
|
221
|
+
row["resource_id"] = f"{cid}~{crit_id}"
|
|
217
222
|
return {"negative_keywords": rows, "total_negative_keywords": len(rows)}
|
|
218
223
|
|
|
219
224
|
|
|
@@ -1626,6 +1626,35 @@ def _draft_status_change(
|
|
|
1626
1626
|
# ---------------------------------------------------------------------------
|
|
1627
1627
|
|
|
1628
1628
|
|
|
1629
|
+
_MUTATE_RESPONSE_RESULT_FIELDS = [
|
|
1630
|
+
"campaign_budget_result",
|
|
1631
|
+
"campaign_result",
|
|
1632
|
+
"ad_group_result",
|
|
1633
|
+
"ad_group_ad_result",
|
|
1634
|
+
"ad_group_criterion_result",
|
|
1635
|
+
"campaign_criterion_result",
|
|
1636
|
+
"asset_result",
|
|
1637
|
+
"campaign_asset_result",
|
|
1638
|
+
"customer_asset_result",
|
|
1639
|
+
]
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def _extract_resource_name(resp: object) -> str:
|
|
1643
|
+
"""Extract the resource_name from a MutateOperationResponse.
|
|
1644
|
+
|
|
1645
|
+
Uses direct field access instead of WhichOneof, which doesn't work on
|
|
1646
|
+
proto-plus wrapped messages returned by the google-ads library.
|
|
1647
|
+
"""
|
|
1648
|
+
for field in _MUTATE_RESPONSE_RESULT_FIELDS:
|
|
1649
|
+
try:
|
|
1650
|
+
result = getattr(resp, field, None)
|
|
1651
|
+
if result and result.resource_name:
|
|
1652
|
+
return result.resource_name
|
|
1653
|
+
except Exception:
|
|
1654
|
+
continue
|
|
1655
|
+
return ""
|
|
1656
|
+
|
|
1657
|
+
|
|
1629
1658
|
def _execute_plan(config: AdLoopConfig, plan: object) -> dict:
|
|
1630
1659
|
"""Dispatch to the right Google Ads mutate call based on plan.operation."""
|
|
1631
1660
|
from adloop.ads.client import get_ads_client, normalize_customer_id
|
|
@@ -1818,22 +1847,20 @@ def _apply_create_campaign(client: object, cid: str, changes: dict) -> dict:
|
|
|
1818
1847
|
num_geo = len(changes.get("geo_target_ids") or [])
|
|
1819
1848
|
num_lang = len(changes.get("language_ids") or [])
|
|
1820
1849
|
for i, resp in enumerate(response.mutate_operation_responses):
|
|
1821
|
-
|
|
1822
|
-
if
|
|
1823
|
-
inner = getattr(resp, resp_type)
|
|
1824
|
-
resource = getattr(inner, "resource_name", str(inner))
|
|
1850
|
+
rn = _extract_resource_name(resp)
|
|
1851
|
+
if rn:
|
|
1825
1852
|
if i == 0:
|
|
1826
|
-
results["campaign_budget"] =
|
|
1853
|
+
results["campaign_budget"] = rn
|
|
1827
1854
|
elif i == 1:
|
|
1828
|
-
results["campaign"] =
|
|
1855
|
+
results["campaign"] = rn
|
|
1829
1856
|
elif i == 2:
|
|
1830
|
-
results["ad_group"] =
|
|
1857
|
+
results["ad_group"] = rn
|
|
1831
1858
|
elif i < 3 + num_keywords:
|
|
1832
|
-
results.setdefault("keywords", []).append(
|
|
1859
|
+
results.setdefault("keywords", []).append(rn)
|
|
1833
1860
|
elif i < 3 + num_keywords + num_geo:
|
|
1834
|
-
results.setdefault("geo_targets", []).append(
|
|
1861
|
+
results.setdefault("geo_targets", []).append(rn)
|
|
1835
1862
|
else:
|
|
1836
|
-
results.setdefault("language_targets", []).append(
|
|
1863
|
+
results.setdefault("language_targets", []).append(rn)
|
|
1837
1864
|
|
|
1838
1865
|
return results
|
|
1839
1866
|
|
|
@@ -1874,14 +1901,12 @@ def _apply_create_ad_group(client: object, cid: str, changes: dict) -> dict:
|
|
|
1874
1901
|
|
|
1875
1902
|
results: dict = {}
|
|
1876
1903
|
for i, resp in enumerate(response.mutate_operation_responses):
|
|
1877
|
-
|
|
1878
|
-
if
|
|
1879
|
-
inner = getattr(resp, resp_type)
|
|
1880
|
-
resource = getattr(inner, "resource_name", str(inner))
|
|
1904
|
+
rn = _extract_resource_name(resp)
|
|
1905
|
+
if rn:
|
|
1881
1906
|
if i == 0:
|
|
1882
|
-
results["ad_group"] =
|
|
1907
|
+
results["ad_group"] = rn
|
|
1883
1908
|
else:
|
|
1884
|
-
results.setdefault("keywords", []).append(
|
|
1909
|
+
results.setdefault("keywords", []).append(rn)
|
|
1885
1910
|
|
|
1886
1911
|
return results
|
|
1887
1912
|
|
|
@@ -2039,11 +2064,7 @@ def _apply_update_campaign(client: object, cid: str, changes: dict) -> dict:
|
|
|
2039
2064
|
|
|
2040
2065
|
results = {"updated": []}
|
|
2041
2066
|
for resp in response.mutate_operation_responses:
|
|
2042
|
-
rn = (
|
|
2043
|
-
resp.campaign_result.resource_name
|
|
2044
|
-
or resp.campaign_budget_result.resource_name
|
|
2045
|
-
or resp.campaign_criterion_result.resource_name
|
|
2046
|
-
)
|
|
2067
|
+
rn = _extract_resource_name(resp)
|
|
2047
2068
|
if rn:
|
|
2048
2069
|
results["updated"].append(rn)
|
|
2049
2070
|
return results
|
|
@@ -150,39 +150,42 @@ def _generate_config_yaml(
|
|
|
150
150
|
) -> str:
|
|
151
151
|
dry_run_str = "true" if require_dry_run else "false"
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
lines = [
|
|
154
|
+
"# AdLoop configuration",
|
|
155
|
+
"# Generated by: adloop init",
|
|
156
|
+
"# Docs: https://github.com/kLOsk/adloop",
|
|
157
|
+
"",
|
|
158
|
+
"google:",
|
|
159
|
+
]
|
|
154
160
|
if project_id:
|
|
155
|
-
|
|
161
|
+
lines.append(f' project_id: "{project_id}"')
|
|
156
162
|
if credentials_path:
|
|
157
|
-
|
|
163
|
+
lines.append(f' credentials_path: "{credentials_path}"')
|
|
158
164
|
else:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
log_file: "~/.adloop/audit.log"
|
|
184
|
-
blocked_operations: []
|
|
185
|
-
""")
|
|
165
|
+
lines.append(" # Using built-in credentials (no credentials_path needed)")
|
|
166
|
+
lines.append(' token_path: "~/.adloop/token.json"')
|
|
167
|
+
lines += [
|
|
168
|
+
"",
|
|
169
|
+
"ga4:",
|
|
170
|
+
f' property_id: "{property_id}"',
|
|
171
|
+
"",
|
|
172
|
+
"ads:",
|
|
173
|
+
f' developer_token: "{developer_token}"',
|
|
174
|
+
f' customer_id: "{customer_id}"',
|
|
175
|
+
" # MCC / Manager Account ID (required if using a manager account)",
|
|
176
|
+
f' login_customer_id: "{login_customer_id}"',
|
|
177
|
+
"",
|
|
178
|
+
"safety:",
|
|
179
|
+
" # Maximum daily budget AdLoop can set (safety cap)",
|
|
180
|
+
f" max_daily_budget: {max_daily_budget}",
|
|
181
|
+
" max_bid_increase_pct: 100",
|
|
182
|
+
" # When true, confirm_and_apply always runs as dry_run regardless of the parameter",
|
|
183
|
+
f" require_dry_run: {dry_run_str}",
|
|
184
|
+
' log_file: "~/.adloop/audit.log"',
|
|
185
|
+
" blocked_operations: []",
|
|
186
|
+
"",
|
|
187
|
+
]
|
|
188
|
+
return "\n".join(lines)
|
|
186
189
|
|
|
187
190
|
|
|
188
191
|
def _generate_cursor_snippet() -> str:
|
|
@@ -291,16 +294,17 @@ def run_init_wizard() -> None:
|
|
|
291
294
|
_print()
|
|
292
295
|
|
|
293
296
|
existing_config = None
|
|
297
|
+
_original_config_backup: str | None = None
|
|
294
298
|
if _CONFIG_PATH.exists():
|
|
295
299
|
_print(f" Found existing config at {_CONFIG_PATH}")
|
|
296
300
|
if not _prompt_bool("Overwrite existing configuration?", default=False):
|
|
297
301
|
_print(" Keeping existing config. Exiting.")
|
|
298
302
|
return
|
|
303
|
+
_original_config_backup = _CONFIG_PATH.read_text()
|
|
299
304
|
try:
|
|
300
305
|
import yaml
|
|
301
306
|
|
|
302
|
-
|
|
303
|
-
existing_config = yaml.safe_load(f) or {}
|
|
307
|
+
existing_config = yaml.safe_load(_original_config_backup) or {}
|
|
304
308
|
except Exception:
|
|
305
309
|
existing_config = {}
|
|
306
310
|
|
|
@@ -361,7 +365,8 @@ def run_init_wizard() -> None:
|
|
|
361
365
|
step_num += 1
|
|
362
366
|
_step_header(step_num, "Authorization & Account Discovery")
|
|
363
367
|
|
|
364
|
-
# Write a
|
|
368
|
+
# Write a temporary config for OAuth + discovery. If the wizard is
|
|
369
|
+
# interrupted after this point, _cleanup_on_cancel restores the original.
|
|
365
370
|
_ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
|
|
366
371
|
temp_config_yaml = _generate_config_yaml(
|
|
367
372
|
project_id=project_id,
|
|
@@ -375,6 +380,40 @@ def run_init_wizard() -> None:
|
|
|
375
380
|
)
|
|
376
381
|
_CONFIG_PATH.write_text(temp_config_yaml)
|
|
377
382
|
|
|
383
|
+
# Everything below uses the temp config for OAuth and discovery.
|
|
384
|
+
# If the wizard is interrupted, restore the original config (or remove
|
|
385
|
+
# the temp) so we never leave a half-baked config on disk.
|
|
386
|
+
try:
|
|
387
|
+
_run_wizard_post_config(
|
|
388
|
+
use_bundled=use_bundled,
|
|
389
|
+
credentials_path=credentials_path,
|
|
390
|
+
project_id=project_id,
|
|
391
|
+
developer_token=developer_token,
|
|
392
|
+
login_customer_id=login_customer_id,
|
|
393
|
+
step_num=step_num,
|
|
394
|
+
_existing=_existing,
|
|
395
|
+
)
|
|
396
|
+
except KeyboardInterrupt:
|
|
397
|
+
if _original_config_backup is not None:
|
|
398
|
+
_CONFIG_PATH.write_text(_original_config_backup)
|
|
399
|
+
elif _CONFIG_PATH.exists():
|
|
400
|
+
_CONFIG_PATH.unlink()
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _run_wizard_post_config(
|
|
405
|
+
*,
|
|
406
|
+
use_bundled: bool,
|
|
407
|
+
credentials_path: str,
|
|
408
|
+
project_id: str,
|
|
409
|
+
developer_token: str,
|
|
410
|
+
login_customer_id: str,
|
|
411
|
+
step_num: int,
|
|
412
|
+
_existing: object,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Run the wizard steps after the temp config has been written."""
|
|
415
|
+
from adloop.config import load_config
|
|
416
|
+
|
|
378
417
|
# Optional: copy custom credentials to ~/.adloop/
|
|
379
418
|
if not use_bundled and credentials_path:
|
|
380
419
|
creds_expanded = Path(credentials_path).expanduser()
|
|
@@ -392,13 +431,14 @@ def run_init_wizard() -> None:
|
|
|
392
431
|
_print()
|
|
393
432
|
oauth_ok = False
|
|
394
433
|
try:
|
|
395
|
-
from adloop.config import load_config
|
|
396
434
|
from adloop.auth import _oauth_flow
|
|
397
435
|
|
|
398
436
|
cfg = load_config(str(_CONFIG_PATH))
|
|
399
437
|
_oauth_flow(cfg)
|
|
400
438
|
_print(" ✓ OAuth token saved")
|
|
401
439
|
oauth_ok = True
|
|
440
|
+
except KeyboardInterrupt:
|
|
441
|
+
raise
|
|
402
442
|
except Exception as exc:
|
|
403
443
|
_print(f" ✗ OAuth failed: {exc}")
|
|
404
444
|
_print(" You can retry later — any AdLoop tool call will trigger auth.")
|
|
@@ -424,10 +464,12 @@ def run_init_wizard() -> None:
|
|
|
424
464
|
else:
|
|
425
465
|
_print(" No GA4 properties found. Enter manually:")
|
|
426
466
|
property_id = _prompt_property_id()
|
|
467
|
+
except KeyboardInterrupt:
|
|
468
|
+
raise
|
|
427
469
|
except Exception as exc:
|
|
428
470
|
_print(f" Could not auto-discover GA4 properties: {exc}")
|
|
429
471
|
property_id = _prompt_property_id(
|
|
430
|
-
default=_existing("ga4", "property_id"),
|
|
472
|
+
default=_existing("ga4", "property_id"), # type: ignore[operator]
|
|
431
473
|
)
|
|
432
474
|
else:
|
|
433
475
|
step_num += 1
|
|
@@ -436,7 +478,7 @@ def run_init_wizard() -> None:
|
|
|
436
478
|
_print(" → https://analytics.google.com → Admin → Property Settings")
|
|
437
479
|
_print()
|
|
438
480
|
property_id = _prompt_property_id(
|
|
439
|
-
default=_existing("ga4", "property_id"),
|
|
481
|
+
default=_existing("ga4", "property_id"), # type: ignore[operator]
|
|
440
482
|
)
|
|
441
483
|
|
|
442
484
|
# Auto-discover Ads accounts
|
|
@@ -460,18 +502,20 @@ def run_init_wizard() -> None:
|
|
|
460
502
|
else:
|
|
461
503
|
_print(" No Ads accounts found. Enter manually:")
|
|
462
504
|
customer_id = _prompt_customer_id("Ads Customer ID (XXX-XXX-XXXX)")
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
raise
|
|
463
507
|
except Exception as exc:
|
|
464
508
|
_print(f" Could not auto-discover Ads accounts: {exc}")
|
|
465
509
|
customer_id = _prompt_customer_id(
|
|
466
510
|
"Ads Customer ID (XXX-XXX-XXXX)",
|
|
467
|
-
default=_existing("ads", "customer_id"),
|
|
511
|
+
default=_existing("ads", "customer_id"), # type: ignore[operator]
|
|
468
512
|
)
|
|
469
513
|
else:
|
|
470
514
|
step_num += 1
|
|
471
515
|
_step_header(step_num, "Google Ads Account")
|
|
472
516
|
customer_id = _prompt_customer_id(
|
|
473
517
|
"Ads Customer ID (XXX-XXX-XXXX)",
|
|
474
|
-
default=_existing("ads", "customer_id"),
|
|
518
|
+
default=_existing("ads", "customer_id"), # type: ignore[operator]
|
|
475
519
|
)
|
|
476
520
|
|
|
477
521
|
# Safety defaults
|
|
@@ -479,7 +523,7 @@ def run_init_wizard() -> None:
|
|
|
479
523
|
_step_header(step_num, "Safety Defaults")
|
|
480
524
|
budget_str = _prompt(
|
|
481
525
|
"Max daily budget cap (safety limit)",
|
|
482
|
-
default=str(_existing("safety", "max_daily_budget", "50")),
|
|
526
|
+
default=str(_existing("safety", "max_daily_budget", "50")), # type: ignore[operator]
|
|
483
527
|
required=False,
|
|
484
528
|
)
|
|
485
529
|
try:
|
|
@@ -1039,7 +1039,8 @@ def remove_entity(
|
|
|
1039
1039
|
"campaign_asset", "asset", or "customer_asset"
|
|
1040
1040
|
entity_id: The resource ID.
|
|
1041
1041
|
For keywords: "adGroupId~criterionId"
|
|
1042
|
-
For negative_keywords:
|
|
1042
|
+
For negative_keywords: "campaignId~criterionId"
|
|
1043
|
+
(use the resource_id field from get_negative_keywords)
|
|
1043
1044
|
For campaign_asset: "campaignId~assetId~fieldType"
|
|
1044
1045
|
For asset: simple asset ID
|
|
1045
1046
|
For customer_asset: "assetId~fieldType"
|
|
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
|