adloop 0.4.2__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.2 → adloop-0.4.3}/PKG-INFO +21 -12
- {adloop-0.4.2 → adloop-0.4.3}/README.md +20 -11
- {adloop-0.4.2 → adloop-0.4.3}/pyproject.toml +1 -1
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/__init__.py +5 -1
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/read.py +5 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/write.py +42 -21
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/cli.py +50 -9
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/server.py +2 -1
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/__main__.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/client.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/currency.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/auth.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/bundled_credentials.json +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/config.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/crossref.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ga4/client.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/safety/audit.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/safety/guards.py +0 -0
- {adloop-0.4.2 → adloop-0.4.3}/src/adloop/safety/preview.py +0 -0
- {adloop-0.4.2 → 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,6 +11,10 @@ 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
|
|
|
@@ -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
|
|
@@ -294,16 +294,17 @@ def run_init_wizard() -> None:
|
|
|
294
294
|
_print()
|
|
295
295
|
|
|
296
296
|
existing_config = None
|
|
297
|
+
_original_config_backup: str | None = None
|
|
297
298
|
if _CONFIG_PATH.exists():
|
|
298
299
|
_print(f" Found existing config at {_CONFIG_PATH}")
|
|
299
300
|
if not _prompt_bool("Overwrite existing configuration?", default=False):
|
|
300
301
|
_print(" Keeping existing config. Exiting.")
|
|
301
302
|
return
|
|
303
|
+
_original_config_backup = _CONFIG_PATH.read_text()
|
|
302
304
|
try:
|
|
303
305
|
import yaml
|
|
304
306
|
|
|
305
|
-
|
|
306
|
-
existing_config = yaml.safe_load(f) or {}
|
|
307
|
+
existing_config = yaml.safe_load(_original_config_backup) or {}
|
|
307
308
|
except Exception:
|
|
308
309
|
existing_config = {}
|
|
309
310
|
|
|
@@ -364,7 +365,8 @@ def run_init_wizard() -> None:
|
|
|
364
365
|
step_num += 1
|
|
365
366
|
_step_header(step_num, "Authorization & Account Discovery")
|
|
366
367
|
|
|
367
|
-
# 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.
|
|
368
370
|
_ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
|
|
369
371
|
temp_config_yaml = _generate_config_yaml(
|
|
370
372
|
project_id=project_id,
|
|
@@ -378,6 +380,40 @@ def run_init_wizard() -> None:
|
|
|
378
380
|
)
|
|
379
381
|
_CONFIG_PATH.write_text(temp_config_yaml)
|
|
380
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
|
+
|
|
381
417
|
# Optional: copy custom credentials to ~/.adloop/
|
|
382
418
|
if not use_bundled and credentials_path:
|
|
383
419
|
creds_expanded = Path(credentials_path).expanduser()
|
|
@@ -395,13 +431,14 @@ def run_init_wizard() -> None:
|
|
|
395
431
|
_print()
|
|
396
432
|
oauth_ok = False
|
|
397
433
|
try:
|
|
398
|
-
from adloop.config import load_config
|
|
399
434
|
from adloop.auth import _oauth_flow
|
|
400
435
|
|
|
401
436
|
cfg = load_config(str(_CONFIG_PATH))
|
|
402
437
|
_oauth_flow(cfg)
|
|
403
438
|
_print(" ✓ OAuth token saved")
|
|
404
439
|
oauth_ok = True
|
|
440
|
+
except KeyboardInterrupt:
|
|
441
|
+
raise
|
|
405
442
|
except Exception as exc:
|
|
406
443
|
_print(f" ✗ OAuth failed: {exc}")
|
|
407
444
|
_print(" You can retry later — any AdLoop tool call will trigger auth.")
|
|
@@ -427,10 +464,12 @@ def run_init_wizard() -> None:
|
|
|
427
464
|
else:
|
|
428
465
|
_print(" No GA4 properties found. Enter manually:")
|
|
429
466
|
property_id = _prompt_property_id()
|
|
467
|
+
except KeyboardInterrupt:
|
|
468
|
+
raise
|
|
430
469
|
except Exception as exc:
|
|
431
470
|
_print(f" Could not auto-discover GA4 properties: {exc}")
|
|
432
471
|
property_id = _prompt_property_id(
|
|
433
|
-
default=_existing("ga4", "property_id"),
|
|
472
|
+
default=_existing("ga4", "property_id"), # type: ignore[operator]
|
|
434
473
|
)
|
|
435
474
|
else:
|
|
436
475
|
step_num += 1
|
|
@@ -439,7 +478,7 @@ def run_init_wizard() -> None:
|
|
|
439
478
|
_print(" → https://analytics.google.com → Admin → Property Settings")
|
|
440
479
|
_print()
|
|
441
480
|
property_id = _prompt_property_id(
|
|
442
|
-
default=_existing("ga4", "property_id"),
|
|
481
|
+
default=_existing("ga4", "property_id"), # type: ignore[operator]
|
|
443
482
|
)
|
|
444
483
|
|
|
445
484
|
# Auto-discover Ads accounts
|
|
@@ -463,18 +502,20 @@ def run_init_wizard() -> None:
|
|
|
463
502
|
else:
|
|
464
503
|
_print(" No Ads accounts found. Enter manually:")
|
|
465
504
|
customer_id = _prompt_customer_id("Ads Customer ID (XXX-XXX-XXXX)")
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
raise
|
|
466
507
|
except Exception as exc:
|
|
467
508
|
_print(f" Could not auto-discover Ads accounts: {exc}")
|
|
468
509
|
customer_id = _prompt_customer_id(
|
|
469
510
|
"Ads Customer ID (XXX-XXX-XXXX)",
|
|
470
|
-
default=_existing("ads", "customer_id"),
|
|
511
|
+
default=_existing("ads", "customer_id"), # type: ignore[operator]
|
|
471
512
|
)
|
|
472
513
|
else:
|
|
473
514
|
step_num += 1
|
|
474
515
|
_step_header(step_num, "Google Ads Account")
|
|
475
516
|
customer_id = _prompt_customer_id(
|
|
476
517
|
"Ads Customer ID (XXX-XXX-XXXX)",
|
|
477
|
-
default=_existing("ads", "customer_id"),
|
|
518
|
+
default=_existing("ads", "customer_id"), # type: ignore[operator]
|
|
478
519
|
)
|
|
479
520
|
|
|
480
521
|
# Safety defaults
|
|
@@ -482,7 +523,7 @@ def run_init_wizard() -> None:
|
|
|
482
523
|
_step_header(step_num, "Safety Defaults")
|
|
483
524
|
budget_str = _prompt(
|
|
484
525
|
"Max daily budget cap (safety limit)",
|
|
485
|
-
default=str(_existing("safety", "max_daily_budget", "50")),
|
|
526
|
+
default=str(_existing("safety", "max_daily_budget", "50")), # type: ignore[operator]
|
|
486
527
|
required=False,
|
|
487
528
|
)
|
|
488
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
|