adloop 0.27.1__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.1 → adloop-0.27.2}/PKG-INFO +1 -1
- {adloop-0.27.1 → adloop-0.27.2}/pyproject.toml +1 -1
- {adloop-0.27.1 → adloop-0.27.2}/pyproject.toml.orig +1 -1
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/write.py +32 -4
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/server.py +10 -5
- {adloop-0.27.1 → adloop-0.27.2}/README.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/__main__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/brands.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/currency.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/enums.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/read.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/shopping.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/auth.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/cli.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/config.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/crossref.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/diagnostics.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/write.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/read.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/write.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/read.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/pagespeed.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/client.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/read.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/write.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/adloop.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules_install.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/runtime.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/approval.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/approval_card.html +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/audit.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/guards.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/preview.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/tracking.py +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/upload_card.html +0 -0
- {adloop-0.27.1 → adloop-0.27.2}/src/adloop/uploads.py +0 -0
|
@@ -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:
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|