adloop 0.34.0__tar.gz → 0.34.1__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 (74) hide show
  1. {adloop-0.34.0 → adloop-0.34.1}/PKG-INFO +1 -1
  2. {adloop-0.34.0 → adloop-0.34.1}/pyproject.toml +1 -1
  3. {adloop-0.34.0 → adloop-0.34.1}/pyproject.toml.orig +1 -1
  4. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/assets.py +27 -10
  5. adloop-0.34.1/src/adloop/ads/snippet_headers.py +723 -0
  6. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/write.py +0 -16
  7. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/adloop.md +1 -1
  8. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/server.py +10 -6
  9. {adloop-0.34.0 → adloop-0.34.1}/README.md +0 -0
  10. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/__init__.py +0 -0
  11. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/__main__.py +0 -0
  12. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/_mcp_patches.py +0 -0
  13. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/__init__.py +0 -0
  14. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/ai_max.py +0 -0
  15. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/audiences.py +0 -0
  16. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/brands.py +0 -0
  17. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/client.py +0 -0
  18. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/conversion_actions.py +0 -0
  19. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/currency.py +0 -0
  20. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/custom_conversion_goals.py +0 -0
  21. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/enums.py +0 -0
  22. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/forecast.py +0 -0
  23. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/gaql.py +0 -0
  24. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/image_fetch.py +0 -0
  25. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/pmax.py +0 -0
  26. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/read.py +0 -0
  27. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/shopping.py +0 -0
  28. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ads/validate_only.py +0 -0
  29. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/auth.py +0 -0
  30. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/cli.py +0 -0
  31. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/config.py +0 -0
  32. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/crossref.py +0 -0
  33. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/diagnostics.py +0 -0
  34. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ga4/__init__.py +0 -0
  35. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ga4/client.py +0 -0
  36. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ga4/reports.py +0 -0
  37. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ga4/tracking.py +0 -0
  38. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/ga4/write.py +0 -0
  39. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gsc/__init__.py +0 -0
  40. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gsc/client.py +0 -0
  41. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gsc/reports.py +0 -0
  42. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gtm/__init__.py +0 -0
  43. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gtm/client.py +0 -0
  44. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gtm/entities.py +0 -0
  45. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gtm/read.py +0 -0
  46. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/gtm/write.py +0 -0
  47. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/merchant/__init__.py +0 -0
  48. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/merchant/client.py +0 -0
  49. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/merchant/read.py +0 -0
  50. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/pagespeed.py +0 -0
  51. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/__init__.py +0 -0
  52. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/auth.py +0 -0
  53. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/client.py +0 -0
  54. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/read.py +0 -0
  55. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/schedule.py +0 -0
  56. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/reddit/write.py +0 -0
  57. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/__init__.py +0 -0
  58. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
  59. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/budget-plan.md +0 -0
  60. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/create-ad.md +0 -0
  61. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/create-campaign.md +0 -0
  62. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  63. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  64. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/rules_install.py +0 -0
  65. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/runtime.py +0 -0
  66. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/__init__.py +0 -0
  67. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/approval.py +0 -0
  68. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/approval_card.html +0 -0
  69. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/audit.py +0 -0
  70. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/guards.py +0 -0
  71. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/safety/preview.py +0 -0
  72. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/tracking.py +0 -0
  73. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/upload_card.html +0 -0
  74. {adloop-0.34.0 → adloop-0.34.1}/src/adloop/uploads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.34.0
3
+ Version: 0.34.1
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.34.0"
3
+ version = "0.34.1"
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.34.0"
3
+ version = "0.34.1"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -151,8 +151,22 @@ def _validate_callout_texts(callouts: list[str]) -> tuple[list[str], list[str]]:
151
151
  return validated, errors
152
152
 
153
153
 
154
- def _validate_snippet_items(snippets: list[dict]) -> tuple[list[dict], list[str]]:
155
- from adloop.ads.write import _STRUCTURED_SNIPPET_HEADERS
154
+ def _header_error(index: int, header: str) -> str:
155
+ from adloop.ads.snippet_headers import ENGLISH_HEADERS
156
+
157
+ return (
158
+ f"Structured snippet {index + 1}: header '{header}' is not one of Google's snippet "
159
+ f"headers. English: {', '.join(ENGLISH_HEADERS)}. Google's translations are "
160
+ "accepted too, in the language of the values (German 'Typen', 'Marken', "
161
+ "'Dienstleistungen'): https://developers.google.com/google-ads/api/data/structured-snippet-headers"
162
+ )
163
+
164
+
165
+ def _validate_snippet_items(
166
+ snippets: list[dict], *, keep_header: str = ""
167
+ ) -> tuple[list[dict], list[str]]:
168
+ """Check snippets; ``keep_header`` is an existing asset's header, accepted as is."""
169
+ from adloop.ads.snippet_headers import canonical_header
156
170
 
157
171
  errors: list[str] = []
158
172
  validated: list[dict] = []
@@ -161,11 +175,12 @@ def _validate_snippet_items(snippets: list[dict]) -> tuple[list[dict], list[str]
161
175
  for index, snippet in enumerate(snippets):
162
176
  header = _clean(snippet.get("header", ""))
163
177
  values = [_clean(value) for value in snippet.get("values", []) or []]
164
- if header not in _STRUCTURED_SNIPPET_HEADERS:
165
- errors.append(
166
- f"Structured snippet {index + 1}: header must be one of "
167
- f"{sorted(_STRUCTURED_SNIPPET_HEADERS)}"
168
- )
178
+ if keep_header and header == keep_header:
179
+ pass # Google already holds this header; it is not ours to reject.
180
+ elif canonical := canonical_header(header):
181
+ header = canonical
182
+ else:
183
+ errors.append(_header_error(index, header))
169
184
  if len(values) < 3 or len(values) > 10:
170
185
  errors.append(f"Structured snippet {index + 1}: values must contain 3-10 items")
171
186
  for value_index, value in enumerate(values):
@@ -543,9 +558,11 @@ def update_structured_snippet(
543
558
  current, error = _asset_for_edit(config, customer_id, asset_id, _SNIPPET_FIELDS, "STRUCTURED_SNIPPET")
544
559
  if error:
545
560
  return error
546
- # An empty header keeps the current one; values are always the full list.
547
- snippet = {"header": _clean(header) or (current or {}).get("header", ""), "values": values or []}
548
- validated, snippet_errors = _validate_snippet_items([snippet])
561
+ # An empty header keeps the current one, accepted as Google stores it;
562
+ # values are always the full list.
563
+ kept = (current or {}).get("header", "")
564
+ snippet = {"header": _clean(header) or kept, "values": values or []}
565
+ validated, snippet_errors = _validate_snippet_items([snippet], keep_header=kept)
549
566
  errors.extend(snippet_errors)
550
567
  if errors:
551
568
  return {"error": "Validation failed", "details": errors}
@@ -0,0 +1,723 @@
1
+ """Structured snippet headers Google accepts: the English names and their translations.
2
+
3
+ A snippet header has to be one of these strings, in the language of the
4
+ snippet's values. Generated from Google's table "Structured Snippet Header
5
+ Translations":
6
+ https://developers.google.com/google-ads/api/data/structured-snippet-headers
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # The English headers, in the column order of the table below.
12
+ ENGLISH_HEADERS = (
13
+ "Brands",
14
+ "Amenities",
15
+ "Styles",
16
+ "Types",
17
+ "Destinations",
18
+ "Services",
19
+ "Courses",
20
+ "Neighborhoods",
21
+ "Shows",
22
+ "Insurance coverage",
23
+ "Degree programs",
24
+ "Featured Hotels",
25
+ "Models",
26
+ )
27
+
28
+ # locale -> (language, headers in ENGLISH_HEADERS order; "" where Google lists none).
29
+ LOCALIZED_HEADERS: dict[str, tuple[str, tuple[str, ...]]] = {
30
+ "ar": ("Arabic", (
31
+ 'العلامات التجارية',
32
+ 'وسائل الراحة',
33
+ 'الأنماط',
34
+ 'الأنواع',
35
+ 'الوجهات',
36
+ 'الخدمات',
37
+ 'الدورات التدريبية',
38
+ 'الأحياء',
39
+ 'العروض',
40
+ 'التغطية التأمينية',
41
+ 'برامج الشهادات',
42
+ 'الفنادق المميّزة',
43
+ 'النماذج',
44
+ )),
45
+ "bg": ("Bulgarian", (
46
+ 'Марки',
47
+ 'Удобства',
48
+ 'Стилове',
49
+ 'Типове',
50
+ 'Дестинации',
51
+ 'Услуги',
52
+ 'Курсове',
53
+ 'Квартали',
54
+ 'Предавания',
55
+ 'Застраховка',
56
+ 'Специалности',
57
+ 'Представени хотели',
58
+ 'Модели',
59
+ )),
60
+ "ca": ("Catalan", (
61
+ 'Marques',
62
+ 'Serveis addicionals',
63
+ 'Estils',
64
+ 'Tipus',
65
+ 'Destinacions',
66
+ 'Serveis',
67
+ 'Cursos',
68
+ 'Barris',
69
+ 'Programes',
70
+ 'Assegurança',
71
+ 'Titulacions',
72
+ 'Hotels destacats',
73
+ 'Models',
74
+ )),
75
+ "zh-HK": ("Chinese (Hong Kong)", (
76
+ '品牌',
77
+ '設施',
78
+ '款式',
79
+ '類型',
80
+ '目的地',
81
+ '服務',
82
+ '課程',
83
+ '社區',
84
+ '節目',
85
+ '保障範圍',
86
+ '學位課程',
87
+ '特色酒店',
88
+ '型號',
89
+ )),
90
+ "zh-CN": ("Chinese (Simplified)", (
91
+ '品牌',
92
+ '设施',
93
+ '款式',
94
+ '类型',
95
+ '目的地',
96
+ '服务',
97
+ '课程',
98
+ '社区',
99
+ '节目',
100
+ '险种',
101
+ '学位课程',
102
+ '精选酒店',
103
+ '型号',
104
+ )),
105
+ "zh-TW": ("Chinese (Traditional)", (
106
+ '品牌',
107
+ '設施',
108
+ '款式',
109
+ '類型',
110
+ '目的地',
111
+ '服務',
112
+ '課程',
113
+ '社區',
114
+ '節目',
115
+ '承保範圍',
116
+ '學位課程',
117
+ '精選飯店',
118
+ '型號',
119
+ )),
120
+ "hr": ("Croatian", (
121
+ 'Robne marke',
122
+ 'Sadržaji',
123
+ 'Stilovi',
124
+ 'Vrste',
125
+ 'Odredišta',
126
+ 'Usluge',
127
+ 'Tečajevi',
128
+ 'Četvrti',
129
+ 'Emisije',
130
+ 'Osiguranje',
131
+ 'Studijski programi',
132
+ 'Istaknuti hoteli',
133
+ 'Modeli',
134
+ )),
135
+ "cs": ("Czech", (
136
+ 'Značky',
137
+ 'Vybavení',
138
+ 'Styly',
139
+ 'Typy',
140
+ 'Cíle',
141
+ 'Služby',
142
+ 'Kurzy',
143
+ 'Známé oblasti',
144
+ 'Pořady',
145
+ 'Pojištění',
146
+ 'Studijní programy',
147
+ 'Vybrané hotely',
148
+ 'Modely',
149
+ )),
150
+ "da": ("Danish", (
151
+ 'Brands',
152
+ 'Faciliteter',
153
+ 'Design',
154
+ 'Typer',
155
+ 'Destinationer',
156
+ 'Tjenester',
157
+ 'Kurser',
158
+ 'Nabolag',
159
+ 'Serier',
160
+ 'Forsikringsdækning',
161
+ 'Uddannelser',
162
+ 'Udvalgte hoteller',
163
+ 'Modeller',
164
+ )),
165
+ "nl": ("Dutch", (
166
+ 'Merken',
167
+ 'Voorzieningen',
168
+ 'Stijlen',
169
+ 'Typen',
170
+ 'Bestemmingen',
171
+ 'Services',
172
+ 'Cursussen',
173
+ 'Buurten',
174
+ 'Shows',
175
+ 'Dekking',
176
+ "Studieprogramma's",
177
+ 'Aanbevolen hotels',
178
+ 'Modellen',
179
+ )),
180
+ "en-AU": ("English (Australia)", (
181
+ 'Brands',
182
+ 'Amenities',
183
+ 'Styles',
184
+ 'Types',
185
+ 'Destinations',
186
+ 'Services',
187
+ 'Courses',
188
+ 'Neighbourhoods',
189
+ 'Shows',
190
+ 'Insurance coverage',
191
+ 'Degree programmes',
192
+ 'Featured hotels',
193
+ 'Models',
194
+ )),
195
+ "en-GB": ("English (UK)", (
196
+ 'Brands',
197
+ 'Amenities',
198
+ 'Styles',
199
+ 'Types',
200
+ 'Destinations',
201
+ 'Services',
202
+ 'Courses',
203
+ 'Neighbourhoods',
204
+ 'Shows',
205
+ 'Insurance coverage',
206
+ 'Degree programmes',
207
+ 'Featured hotels',
208
+ 'Models',
209
+ )),
210
+ "et": ("Estonian", (
211
+ 'Brändid',
212
+ 'Mugavusteenused',
213
+ 'Stiilid',
214
+ 'Tüübid',
215
+ 'Sihtkohad',
216
+ 'Teenused',
217
+ 'Kursused',
218
+ 'Ümbruskonnad',
219
+ 'Saated ja etendused',
220
+ 'Kindlustuskaitse',
221
+ 'Kraadiõppeprogrammid',
222
+ 'Soovitatud hotellid',
223
+ 'Mudelid',
224
+ )),
225
+ "fil": ("Filipino", (
226
+ 'Mga Brand',
227
+ 'Mga Amenity',
228
+ 'Mga Istilo',
229
+ 'Mga Uri',
230
+ 'Mga Destinasyon',
231
+ 'Mga Serbisyo',
232
+ 'Mga Kurso',
233
+ 'Mga Komunidad',
234
+ 'Mga Palabas',
235
+ 'Sakop ng insurance',
236
+ 'Mga degree program',
237
+ 'Itinatampok na hotel',
238
+ 'Mga Modelo',
239
+ )),
240
+ "fi": ("Finnish", (
241
+ 'Brändit',
242
+ 'Lisäpalvelut',
243
+ 'Tyylit',
244
+ 'Tyypit',
245
+ 'Kohteet',
246
+ 'Palvelut',
247
+ 'Kurssit',
248
+ 'Kaupunginosat',
249
+ "Show't",
250
+ 'Vakuutukset',
251
+ 'Oppiaineet',
252
+ 'Suositellut hotellit',
253
+ 'Automerkit',
254
+ )),
255
+ "fr": ("French (France)", (
256
+ 'Marques',
257
+ 'Équipements',
258
+ 'Styles',
259
+ 'Types',
260
+ 'Destinations',
261
+ 'Services',
262
+ 'Cours',
263
+ 'Quartiers',
264
+ 'Émissions',
265
+ "Couverture d'assurance",
266
+ "Programmes d'études",
267
+ "Sélection d'hôtels",
268
+ 'Modèles',
269
+ )),
270
+ "de": ("German", (
271
+ 'Marken',
272
+ 'Ausstattung',
273
+ 'Stile',
274
+ 'Typen',
275
+ 'Ziele',
276
+ 'Dienstleistungen',
277
+ 'Kurse',
278
+ 'Viertel',
279
+ 'Serien',
280
+ 'Versicherungsleistung',
281
+ 'Studiengänge',
282
+ 'Vorgestellte Hotels',
283
+ 'Modelle',
284
+ )),
285
+ "el": ("Greek", (
286
+ 'Επωνυμίες',
287
+ 'Παροχές',
288
+ 'Στυλ',
289
+ 'Τύποι',
290
+ 'Προορισμοί',
291
+ 'Υπηρεσίες',
292
+ 'Μαθήματα',
293
+ 'Γειτονιές',
294
+ 'Εκπομπές',
295
+ 'Ασφαλιστική κάλυψη',
296
+ 'Προγράμματα πτυχίων',
297
+ 'Ξενοδοχεία',
298
+ 'Μοντέλα',
299
+ )),
300
+ "iw": ("Hebrew", (
301
+ 'מותגים',
302
+ 'שירותי המקום',
303
+ 'סגנונות',
304
+ 'סוגים',
305
+ 'יעדים גיאוגרפיים',
306
+ 'שירותים',
307
+ 'קורסים',
308
+ 'שכונות',
309
+ 'הופעות ותוכניות',
310
+ 'כיסוי ביטוחי',
311
+ 'תוכניות ללימודי תואר',
312
+ 'מלונות מומלצים',
313
+ 'דגמים',
314
+ )),
315
+ "hi": ("Hindi", (
316
+ 'ब्रांड',
317
+ 'सुविधाएं',
318
+ 'शैलियां',
319
+ 'प्रकार',
320
+ 'गंतव्य',
321
+ 'सेवाएं',
322
+ 'पाठ्\u200dयक्रम',
323
+ 'आस-पड़ोस',
324
+ 'शो',
325
+ 'बीमा कवरेज',
326
+ 'डिग्री कार्यक्रम',
327
+ 'प्रदर्शित होटल',
328
+ 'मॉडल',
329
+ )),
330
+ "hu": ("Hungarian", (
331
+ 'Márkák',
332
+ 'Szolgáltatások',
333
+ 'Stílusok',
334
+ 'Típusok',
335
+ 'Úti célok',
336
+ 'Szolgáltatások',
337
+ 'Tanfolyamok',
338
+ 'Városrészek',
339
+ 'Műsorok',
340
+ 'Biztosítási módozat',
341
+ 'Diplomaprogramok',
342
+ 'Kiemelt szállodák',
343
+ 'Modellek',
344
+ )),
345
+ "id": ("Indonesian", (
346
+ 'Merek',
347
+ 'Fasilitas',
348
+ 'Gaya',
349
+ 'Jenis',
350
+ 'Destinasi',
351
+ 'Layanan',
352
+ 'Mata pelajaran',
353
+ 'Kawasan',
354
+ 'Acara',
355
+ 'Cakupan asuransi',
356
+ 'Program sarjana',
357
+ 'Hotel pilihan',
358
+ 'Model',
359
+ )),
360
+ "it": ("Italian", (
361
+ 'Brand',
362
+ 'Servizi',
363
+ 'Stili',
364
+ 'Tipi',
365
+ 'Destinazioni',
366
+ 'Servizi',
367
+ 'Corsi',
368
+ 'Quartieri',
369
+ 'Programmi',
370
+ 'Coperture assicurativa',
371
+ 'Corsi di laurea',
372
+ 'Hotel consigliati',
373
+ 'Modelli',
374
+ )),
375
+ "ja": ("Japanese", (
376
+ 'ブランド',
377
+ '設備',
378
+ 'スタイル',
379
+ 'タイプ',
380
+ '到着地',
381
+ 'サービス',
382
+ 'コース',
383
+ '周辺地域',
384
+ '番組',
385
+ '保険の保障',
386
+ '学位プログラム',
387
+ 'おすすめのホテル',
388
+ 'モデル',
389
+ )),
390
+ "ko": ("Korean", (
391
+ '브랜드',
392
+ '편의 시설',
393
+ '스타일',
394
+ '유형',
395
+ '목적지',
396
+ '서비스',
397
+ '과정',
398
+ '인근 지역',
399
+ '프로그램',
400
+ '보험 보상 범위',
401
+ '학위 취득 프로그램',
402
+ '추천 호텔',
403
+ '모델',
404
+ )),
405
+ "lv": ("Latvian", (
406
+ 'Zīmoli',
407
+ 'Ērtības',
408
+ 'Stili',
409
+ 'Veidi',
410
+ 'Galamērķi',
411
+ 'Pakalpojumi',
412
+ 'Kursi',
413
+ 'Apkaimes',
414
+ 'Pārraides',
415
+ 'Apdrošināšana',
416
+ 'Studiju programmas',
417
+ 'Piedāvātās viesnīcas',
418
+ 'Modeļi',
419
+ )),
420
+ "lt": ("Lithuanian", (
421
+ 'Prekių ženklai',
422
+ 'Patogumai',
423
+ 'Stiliai',
424
+ 'Tipai',
425
+ 'Paskirties vietos',
426
+ 'Paslaugos',
427
+ 'Kursai',
428
+ 'Namų apylinkės',
429
+ 'Laidos',
430
+ 'Draudimo aprėptis',
431
+ 'Moksliniai laipsniai',
432
+ 'Siūlomi viešbučiai',
433
+ 'Modeliai',
434
+ )),
435
+ "ms": ("Malay", (
436
+ 'Jenama',
437
+ 'Kemudahan',
438
+ 'Gaya',
439
+ 'Jenis',
440
+ 'Destinasi',
441
+ 'Perkhidmatan',
442
+ 'Kursus',
443
+ 'Kejiranan',
444
+ 'Rancangan',
445
+ 'Liputan insurans',
446
+ 'Program ijazah',
447
+ 'Hotel ditampilkan',
448
+ 'Model',
449
+ )),
450
+ "no": ("Norwegian", (
451
+ 'Merkevarer',
452
+ 'Fasiliteter',
453
+ 'Stiler',
454
+ 'Typer',
455
+ 'Destinasjoner',
456
+ 'Tjenester',
457
+ 'Kurs',
458
+ 'Lokalområder',
459
+ 'Programmer',
460
+ 'Forsikringsdekning',
461
+ 'Utdanningsprogrammer',
462
+ 'Utvalgte hoteller',
463
+ 'Modeller',
464
+ )),
465
+ "pl": ("Polish", (
466
+ 'Marki',
467
+ 'Udogodnienia',
468
+ 'Style',
469
+ 'Typy',
470
+ 'Miejsca',
471
+ 'Usługi',
472
+ 'Kursy',
473
+ 'Dzielnice',
474
+ 'Programy',
475
+ 'Ubezpieczenia',
476
+ 'Kierunki studiów',
477
+ 'Polecane hotele',
478
+ 'Modele',
479
+ )),
480
+ "pt-BR": ("Portuguese (Brazil)", (
481
+ 'Marcas',
482
+ 'Comodidades',
483
+ 'Estilos',
484
+ 'Tipos',
485
+ 'Destinos',
486
+ 'Serviços',
487
+ 'Cursos',
488
+ 'Bairros',
489
+ 'Programas',
490
+ 'Cobertura do seguro',
491
+ 'Programas de graduação',
492
+ 'Hotéis em destaque',
493
+ 'Modelos',
494
+ )),
495
+ "pt-PT": ("Portuguese (Portugal)", (
496
+ 'Marcas',
497
+ 'Comodidades',
498
+ 'Estilos',
499
+ 'Tipos',
500
+ 'Destinos',
501
+ 'Serviços',
502
+ 'Cursos',
503
+ 'Arredores',
504
+ 'Programas',
505
+ 'Cobertura do seguro',
506
+ 'Licenciaturas',
507
+ 'Hotéis em destaque',
508
+ 'Modelos',
509
+ )),
510
+ "ro": ("Romanian", (
511
+ 'Mărci',
512
+ 'Dotări',
513
+ 'Stiluri',
514
+ 'Tipuri',
515
+ 'Destinații',
516
+ 'Servicii',
517
+ 'Cursuri',
518
+ 'Cartiere',
519
+ 'Emisiuni',
520
+ 'Asigurare',
521
+ 'Programe de studiu',
522
+ 'Hoteluri prezentate',
523
+ 'Modele',
524
+ )),
525
+ "ru": ("Russian", (
526
+ 'Бренды',
527
+ 'Удобства',
528
+ 'Стили',
529
+ 'Типы',
530
+ 'Места',
531
+ 'Услуги',
532
+ 'Курсы',
533
+ 'Районы',
534
+ 'Шоу',
535
+ 'Страховая защита',
536
+ 'Программы высшего образования',
537
+ 'Рекомендуемые отели',
538
+ 'Модели',
539
+ )),
540
+ "sr": ("Serbian", (
541
+ 'брендови',
542
+ 'садржаји',
543
+ 'стилови',
544
+ 'типови',
545
+ 'дестинације',
546
+ 'услуге',
547
+ 'курсеви',
548
+ 'делови града',
549
+ 'емисије',
550
+ 'осигурање',
551
+ 'дипломе',
552
+ 'истакнути хотели',
553
+ 'модели',
554
+ )),
555
+ "sk": ("Slovak", (
556
+ 'Značky',
557
+ 'Vybavenie',
558
+ 'Štýly',
559
+ 'Typy',
560
+ 'Destinácie',
561
+ 'Služby',
562
+ 'Kurzy',
563
+ 'Štvrte',
564
+ 'Relácie',
565
+ 'Poistenie',
566
+ 'Vzdelávacie programy',
567
+ 'Odporúčané hotely',
568
+ 'Modely',
569
+ )),
570
+ "sl": ("Slovenian", (
571
+ 'Blagovne znamke',
572
+ 'Ponudba',
573
+ 'Slogi',
574
+ 'Vrste',
575
+ 'Cilji',
576
+ 'Storitve',
577
+ 'Tečaji',
578
+ 'Soseske',
579
+ 'Oddaje',
580
+ 'Kritje zavarovanj',
581
+ 'Študijski programi',
582
+ 'Predstavljeni hoteli',
583
+ 'Modeli',
584
+ )),
585
+ "es-419": ("Spanish (Latin America)", (
586
+ 'Marcas',
587
+ 'Servicios adicionales',
588
+ 'Estilos',
589
+ 'Tipos',
590
+ 'Destinos',
591
+ 'Servicios',
592
+ 'Cursos',
593
+ 'Barrios',
594
+ 'Programas',
595
+ 'Cobertura de seguro',
596
+ 'Carreras universitarias',
597
+ 'Hoteles destacados',
598
+ 'Modelos',
599
+ )),
600
+ "es": ("Spanish (Spain)", (
601
+ 'Marcas',
602
+ 'Servicios adicionales',
603
+ 'Estilos',
604
+ 'Tipos',
605
+ 'Destinos',
606
+ 'Servicios',
607
+ 'Cursos',
608
+ '',
609
+ 'Espectáculos',
610
+ 'Cobertura de seguro',
611
+ 'Carreras universitarias',
612
+ 'Hoteles destacados',
613
+ 'Modelos',
614
+ )),
615
+ "sv": ("Swedish", (
616
+ 'Varumärken',
617
+ 'Faciliteter',
618
+ 'Stilar',
619
+ 'Typer',
620
+ 'Resmål',
621
+ 'Tjänster',
622
+ 'Kurser',
623
+ 'Barrios',
624
+ 'Program',
625
+ 'Försäkringstyper',
626
+ 'Utbildningsprogram',
627
+ 'Utvalda hotell',
628
+ 'Modeller',
629
+ )),
630
+ "th": ("Thai", (
631
+ 'แบรนด์',
632
+ 'สิ่งอำนวยความสะดวก',
633
+ 'สไตล์',
634
+ 'ประเภท',
635
+ 'ปลายทาง',
636
+ 'บริการ',
637
+ 'หลักสูตร',
638
+ 'ย่านใกล้เคียง',
639
+ 'โชว์',
640
+ 'ความคุ้มครองในประกัน',
641
+ 'หลักสูตรปริญญา',
642
+ 'โรงแรมเด่น',
643
+ 'รุ่น',
644
+ )),
645
+ "tr": ("Turkish", (
646
+ 'Markalar',
647
+ 'Sunulan olanaklar',
648
+ 'Stiller',
649
+ 'Türler',
650
+ 'destinasyonlar',
651
+ 'Hizmetler',
652
+ 'Kurslar',
653
+ 'Mahalleler',
654
+ 'Programlar',
655
+ 'Sigorta kapsamı',
656
+ 'Lisans programları',
657
+ 'Öne çıkan oteller',
658
+ 'Modeller',
659
+ )),
660
+ "uk": ("Ukrainian", (
661
+ 'Бренди',
662
+ 'Зручності',
663
+ 'Стилі',
664
+ 'Типи',
665
+ 'Місця',
666
+ 'Послуги',
667
+ 'Курси',
668
+ 'Квартали',
669
+ 'Шоу',
670
+ 'Страховий захист',
671
+ 'Освітні програми',
672
+ 'Рекомендовані готелі',
673
+ 'Моделі',
674
+ )),
675
+ "vi": ("Vietnamese", (
676
+ 'Thương hiệu',
677
+ 'Tiện nghi',
678
+ 'Kiểu',
679
+ 'Loại',
680
+ 'Điểm đến',
681
+ 'Dịch vụ',
682
+ 'Khóa học',
683
+ 'Vùng lân cận',
684
+ 'Chương trình',
685
+ 'Phạm vi bảo hiểm',
686
+ 'Cấp bằng',
687
+ 'Khách sạn nổi bật',
688
+ 'Model',
689
+ )),
690
+ }
691
+
692
+
693
+ ALL_HEADERS = frozenset(ENGLISH_HEADERS) | frozenset(
694
+ header for _language, headers in LOCALIZED_HEADERS.values() for header in headers if header
695
+ )
696
+
697
+ # casefolded header -> a spelling Google lists. Only a fallback: two listed
698
+ # headers differ by case alone ("Featured Hotels" / en-GB "Featured hotels",
699
+ # Bulgarian "Модели" / Serbian "модели"), so an exact match always wins.
700
+ _CASEFOLDED: dict[str, str] = {}
701
+ for _header in sorted(ALL_HEADERS, key=lambda h: (h not in ENGLISH_HEADERS, h)):
702
+ _CASEFOLDED.setdefault(_header.casefold(), _header)
703
+
704
+
705
+ def canonical_header(header: str) -> str | None:
706
+ """The listed spelling of ``header`` (exact first, then any case), or None."""
707
+ header = (header or "").strip()
708
+ if header in ALL_HEADERS:
709
+ return header
710
+ return _CASEFOLDED.get(header.casefold())
711
+
712
+
713
+ def english_header(header: str) -> str | None:
714
+ """The English header a listed header translates (``"Typen"`` -> ``"Types"``)."""
715
+ canonical = canonical_header(header)
716
+ if canonical is None:
717
+ return None
718
+ if canonical in ENGLISH_HEADERS:
719
+ return canonical
720
+ for _language, headers in LOCALIZED_HEADERS.values():
721
+ if canonical in headers:
722
+ return ENGLISH_HEADERS[headers.index(canonical)]
723
+ return None
@@ -16,22 +16,6 @@ if TYPE_CHECKING:
16
16
  from adloop.config import AdLoopConfig
17
17
 
18
18
 
19
- _STRUCTURED_SNIPPET_HEADERS = {
20
- "Amenities",
21
- "Brands",
22
- "Courses",
23
- "Degree programs",
24
- "Destinations",
25
- "Featured Hotels",
26
- "Insurance coverage",
27
- "Models",
28
- "Neighborhoods",
29
- "Services",
30
- "Shows",
31
- "Styles",
32
- "Types",
33
- }
34
-
35
19
  _VALID_IMAGE_MIME_TYPES = {
36
20
  "image/gif": "IMAGE_GIF",
37
21
  "image/jpeg": "IMAGE_JPEG",
@@ -287,7 +287,7 @@ Conversions that happen offline (a sales call, a lead that turned into a deal) g
287
287
  | `update_ad_group` | Update ad group name and/or MANUAL_CPC `max_cpc` | `ad_group_id`, optional `ad_group_name`, optional `max_cpc` |
288
288
  | `draft_responsive_search_ad` | Create RSA preview (does NOT publish) | 3-15 headlines (≤30 chars), 2-4 descriptions (≤90 chars), final_url required, path1/path2 (≤15 chars each). Each headline/description may be a plain string (unpinned) or `{"text": "...", "pinned_field": "HEADLINE_1"}` (pinned). Valid pin slots: `HEADLINE_1/2/3`, `DESCRIPTION_1/2`. Several assets may pin to one slot (they rotate there), but every slot must stay fillable: by an asset pinned to it or by a distinct unpinned asset. |
289
289
  | `draft_callouts` | Create callout assets for an ad group, a campaign or the account (does NOT publish) | `campaign_id` or `ad_group_id` (the scope follows the id), or `scope="account"`; `callouts` list with 1-25 chars each |
290
- | `draft_structured_snippets` | Create structured snippet assets for an ad group, a campaign or the account (does NOT publish) | `campaign_id` or `ad_group_id`, or `scope="account"`; `snippets` list of `{header, values}` with official header values and 3-10 values |
290
+ | `draft_structured_snippets` | Create structured snippet assets for an ad group, a campaign or the account (does NOT publish) | `campaign_id` or `ad_group_id`, or `scope="account"`; `snippets` list of `{header, values}`: an official header in the language of the values (English `Types`, or Google's translation such as German `Typen`) and 3-10 values |
291
291
  | `draft_business_name_asset` | Create the business name shown with the ads, for a campaign or the account | `business_name` (≤25), `campaign_id` or `scope="account"` |
292
292
  | `link_asset_to_customer` | Link EXISTING assets account-wide (CustomerAsset) | `links`: `{asset_id, field_type}` (CALLOUT, SITELINK, STRUCTURED_SNIPPET, CALL, PRICE, PROMOTION, BUSINESS_NAME, BUSINESS_LOGO, MOBILE_APP, HOTEL_CALLOUT); type is checked |
293
293
  | `update_callout` / `update_sitelink` / `update_structured_snippet` | Edit an existing asset IN PLACE: it keeps its ID and history and changes everywhere it is linked | `asset_id` plus the new values (sitelink: only the fields passed; snippet: the full `values` list, empty `header` keeps it) |
@@ -4239,10 +4239,13 @@ def draft_structured_snippets(
4239
4239
 
4240
4240
  Args:
4241
4241
  snippets: At least one dict with "header" and "values". header is one of
4242
- Amenities, Brands, Courses, Degree programs, Destinations, Featured
4243
- Hotels, Insurance coverage, Models, Neighborhoods, Services, Shows,
4244
- Styles, Types (exact spelling). values is a list of 3-10 non-empty
4245
- strings, each at most 25 characters.
4242
+ Google's snippet headers, in the language of the values: Amenities,
4243
+ Brands, Courses, Degree programs, Destinations, Featured Hotels,
4244
+ Insurance coverage, Models, Neighborhoods, Services, Shows, Styles,
4245
+ Types, or Google's translation of one (German: Typen, Marken,
4246
+ Dienstleistungen, Modelle, Kurse, ...; case does not matter).
4247
+ values is a list of 3-10 non-empty strings, each at most 25
4248
+ characters.
4246
4249
  campaign_id: Numeric ID of the campaign to link the snippets to.
4247
4250
  ad_group_id: Numeric ID of the ad group to link them to, instead of a
4248
4251
  campaign.
@@ -4412,8 +4415,9 @@ def update_structured_snippet(
4412
4415
  asset_id: Numeric ID of the structured snippet asset.
4413
4416
  values: The complete new list of 3-10 values, each at most 25
4414
4417
  characters.
4415
- header: New header (same list as draft_structured_snippets); empty
4416
- keeps the current one.
4418
+ header: New header (English or translated, as in
4419
+ draft_structured_snippets); empty keeps the current one as Google
4420
+ stores it, whatever its language.
4417
4421
  customer_id: Google Ads customer ID (digits, dashes optional). Defaults to
4418
4422
  the configured account.
4419
4423
  """
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