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.
Files changed (71) hide show
  1. {adloop-0.27.1 → adloop-0.27.2}/PKG-INFO +1 -1
  2. {adloop-0.27.1 → adloop-0.27.2}/pyproject.toml +1 -1
  3. {adloop-0.27.1 → adloop-0.27.2}/pyproject.toml.orig +1 -1
  4. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/write.py +32 -4
  5. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/server.py +10 -5
  6. {adloop-0.27.1 → adloop-0.27.2}/README.md +0 -0
  7. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/__init__.py +0 -0
  8. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/__main__.py +0 -0
  9. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/_mcp_patches.py +0 -0
  10. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/__init__.py +0 -0
  11. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/ai_max.py +0 -0
  12. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/brands.py +0 -0
  13. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/client.py +0 -0
  14. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/conversion_actions.py +0 -0
  15. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/currency.py +0 -0
  16. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/custom_conversion_goals.py +0 -0
  17. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/enums.py +0 -0
  18. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/forecast.py +0 -0
  19. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/gaql.py +0 -0
  20. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/image_fetch.py +0 -0
  21. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/pmax.py +0 -0
  22. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/read.py +0 -0
  23. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/shopping.py +0 -0
  24. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ads/validate_only.py +0 -0
  25. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/auth.py +0 -0
  26. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/cli.py +0 -0
  27. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/config.py +0 -0
  28. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/crossref.py +0 -0
  29. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/diagnostics.py +0 -0
  30. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/__init__.py +0 -0
  31. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/client.py +0 -0
  32. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/reports.py +0 -0
  33. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/tracking.py +0 -0
  34. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/ga4/write.py +0 -0
  35. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/__init__.py +0 -0
  36. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/client.py +0 -0
  37. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gsc/reports.py +0 -0
  38. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/__init__.py +0 -0
  39. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/client.py +0 -0
  40. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/entities.py +0 -0
  41. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/read.py +0 -0
  42. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/gtm/write.py +0 -0
  43. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/__init__.py +0 -0
  44. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/client.py +0 -0
  45. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/merchant/read.py +0 -0
  46. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/pagespeed.py +0 -0
  47. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/__init__.py +0 -0
  48. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/auth.py +0 -0
  49. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/client.py +0 -0
  50. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/read.py +0 -0
  51. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/schedule.py +0 -0
  52. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/reddit/write.py +0 -0
  53. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/__init__.py +0 -0
  54. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/adloop.md +0 -0
  55. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
  56. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/budget-plan.md +0 -0
  57. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/create-ad.md +0 -0
  58. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/create-campaign.md +0 -0
  59. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  60. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  61. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/rules_install.py +0 -0
  62. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/runtime.py +0 -0
  63. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/__init__.py +0 -0
  64. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/approval.py +0 -0
  65. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/approval_card.html +0 -0
  66. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/audit.py +0 -0
  67. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/guards.py +0 -0
  68. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/safety/preview.py +0 -0
  69. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/tracking.py +0 -0
  70. {adloop-0.27.1 → adloop-0.27.2}/src/adloop/upload_card.html +0 -0
  71. {adloop-0.27.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.1"
3
+ version = "0.27.2"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.1"
3
+ version = "0.27.2"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- if len(text) > 30:
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 ({len(text)}): '{text}'"
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
- if len(text) > 90:
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 ({len(text)}): '{text}'"
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 as
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