adloop 0.1.0__tar.gz → 0.2.0__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 (27) hide show
  1. adloop-0.1.0/README.md → adloop-0.2.0/PKG-INFO +65 -19
  2. adloop-0.1.0/PKG-INFO → adloop-0.2.0/README.md +41 -39
  3. {adloop-0.1.0 → adloop-0.2.0}/pyproject.toml +7 -1
  4. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/__init__.py +1 -1
  5. adloop-0.2.0/src/adloop/ads/currency.py +79 -0
  6. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ads/gaql.py +10 -0
  7. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ads/read.py +15 -7
  8. adloop-0.2.0/src/adloop/ads/write.py +2413 -0
  9. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/crossref.py +6 -2
  10. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/server.py +307 -25
  11. adloop-0.1.0/src/adloop/ads/write.py +0 -950
  12. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/__main__.py +0 -0
  13. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ads/__init__.py +0 -0
  14. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ads/client.py +0 -0
  15. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ads/forecast.py +0 -0
  16. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/auth.py +0 -0
  17. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/cli.py +0 -0
  18. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/config.py +0 -0
  19. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ga4/__init__.py +0 -0
  20. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ga4/client.py +0 -0
  21. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ga4/reports.py +0 -0
  22. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/ga4/tracking.py +0 -0
  23. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/safety/__init__.py +0 -0
  24. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/safety/audit.py +0 -0
  25. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/safety/guards.py +0 -0
  26. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/safety/preview.py +0 -0
  27. {adloop-0.1.0 → adloop-0.2.0}/src/adloop/tracking.py +0 -0
@@ -1,9 +1,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: adloop
3
+ Version: 0.2.0
4
+ Summary: MCP server connecting Google Ads + GA4 + codebase into one AI-driven feedback loop
5
+ Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
+ Author: Daniel Klose
7
+ Author-email: Daniel Klose <info@daniel-klose.com>
8
+ License: MIT
9
+ Requires-Dist: fastmcp>=3.0.0
10
+ Requires-Dist: google-ads>=29.0.0
11
+ Requires-Dist: google-analytics-data>=0.20.0
12
+ Requires-Dist: google-analytics-admin>=0.27.0
13
+ Requires-Dist: google-auth-oauthlib>=1.0.0
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
17
+ Requires-Python: >=3.11
18
+ Project-URL: Homepage, https://github.com/kLOsk/adloop
19
+ Project-URL: Repository, https://github.com/kLOsk/adloop
20
+ Project-URL: Issues, https://github.com/kLOsk/adloop/issues
21
+ Project-URL: Changelog, https://github.com/kLOsk/adloop/releases
22
+ Provides-Extra: dev
23
+ Description-Content-Type: text/markdown
24
+
1
25
  <div align="center">
2
26
 
3
27
  # AdLoop
4
28
 
5
- **MCP server that connects Google Ads + Google Analytics (GA4) into one AI-driven feedback loop inside your IDE.**
29
+ **Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.**
6
30
 
31
+ [![PyPI](https://img.shields.io/pypi/v/adloop.svg)](https://pypi.org/project/adloop/)
7
32
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
33
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
9
34
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-8A2BE2.svg)](https://modelcontextprotocol.io)
@@ -11,25 +36,39 @@
11
36
  [![GA4 Data API](https://img.shields.io/badge/GA4-Data%20API-E37400.svg?logo=google-analytics&logoColor=white)](https://developers.google.com/analytics/devguides/reporting/data/v1)
12
37
  [![GitHub stars](https://img.shields.io/github/stars/kLOsk/adloop?style=social)](https://github.com/kLOsk/adloop)
13
38
 
14
- *26 tools · Read + Write · Safety-first · GDPR-aware*
39
+ An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
15
40
 
16
- > "You built your product with AI. Now manage your ads the same way."
41
+ `pip install adloop`
17
42
 
18
43
  </div>
19
44
 
20
45
  ---
21
46
 
22
- ## Why
47
+ ## What It Solves
48
+
49
+ AdLoop exists because managing Google Ads alongside your code is a mess. These are the specific problems it handles:
50
+
51
+ - **"My conversions dropped and I don't know why."** AdLoop cross-references Ads clicks, GA4 sessions, and conversion events in one query. It detects whether the gap is from GDPR consent rejection, broken tracking, or an actual landing page problem — before you waste hours checking each dashboard separately.
52
+
53
+ - **"I'm wasting ad spend on irrelevant searches."** Pull your search terms report, identify the junk, and add negative keywords — all from a single conversation in your IDE. No context-switching to the Ads UI.
54
+
55
+ - **"Is my tracking even working?"** Compare the event names in your actual codebase against what GA4 is receiving. Find the mismatches: events you fire that GA4 never sees, events GA4 records that you didn't know about.
23
56
 
24
- Solo founders and small teams ship products fast with AI-assisted coding — but managing Google Ads still means switching between the Ads UI, GA4 dashboards, and your code editor. Whether you're running a SaaS, an e-commerce store, a local service, or anything else you drive traffic to with Google Ads the workflow is the same fragmented mess.
57
+ - **"I need to create ads but the Google Ads UI is hostile."** Draft responsive search ads, create campaigns, add keywords all through natural language. Every change shows a preview first. Nothing goes live without your explicit confirmation. New ads and campaigns start paused.
25
58
 
26
- AdLoop brings the entire **build ship market measure iterate** cycle into your IDE.
59
+ - **"My landing page gets paid traffic but nobody converts."** AdLoop joins your ad final URLs with GA4 page-level data. See which pages get clicks but no conversions, which have high bounce rates, and which ones are orphaned from any ad campaign.
27
60
 
28
- One MCP server gives your AI assistant access to both Google Analytics and Google Ads (read + write), with a safety layer that prevents accidental spend. Combined with your codebase context, it can do things no dashboard can like diagnosing why conversions dropped by cross-referencing ad traffic, analytics events, and your actual frontend code.
61
+ - **"I don't know if my EU consent setup is causing data gaps."** In Europe, 30-70% of users reject analytics cookies. AdLoop accounts for this automaticallyit won't diagnose a normal GDPR consent gap as broken tracking.
29
62
 
30
- ## What's Included 26 Tools
63
+ ## Built From Real Usage
64
+
65
+ Every tool exists because of an actual problem hit while running real Google Ads campaigns. The cross-reference tools exist because we kept manually asking the AI to "get Ads data, then get GA4 data, then compare them" — so we automated the join. The Broad Match + Manual CPC safety rule exists because the AI once created that exact combination and wasted budget. The GDPR consent awareness exists because the AI kept diagnosing normal EU cookie rejection as broken tracking.
66
+
67
+ The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **open an issue describing your situation** — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
31
68
 
32
- > **Quick start:** `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
69
+ ## All 33 Tools
70
+
71
+ > **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
33
72
 
34
73
  ### Diagnostics
35
74
 
@@ -87,8 +126,14 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
87
126
 
88
127
  | Tool | What It Does |
89
128
  |------|-------------|
90
- | `draft_campaign` | Create a full campaign structure — budget + campaign (PAUSED) + ad group + optional keywords. Validates bidding strategy, enforces budget caps, rejects unsafe BROAD match + Manual CPC combinations. |
129
+ | `draft_campaign` | Create a full campaign structure — budget + campaign (PAUSED) + ad group + optional keywords. Supports Search partners, display expansion, and `max_cpc` for either MANUAL_CPC initial ad-group bids or TARGET_SPEND (Maximize Clicks) CPC caps. |
130
+ | `update_campaign` | Modify existing campaign settings — bidding, budget, geo/language targeting, Search partners, display expansion, and TARGET_SPEND (Maximize Clicks) `max_cpc` caps. |
131
+ | `draft_ad_group` | Create a paused SEARCH_STANDARD ad group inside an existing campaign, with optional MANUAL_CPC `max_cpc`. |
132
+ | `update_ad_group` | Update an ad group name and/or MANUAL_CPC `max_cpc`. Use `pause_entity` / `enable_entity` for ad-group status changes. |
91
133
  | `draft_responsive_search_ad` | Create RSA preview (3-15 headlines ≤30 chars, 2-4 descriptions ≤90 chars). Warns if headline/description count is below best practice. |
134
+ | `draft_callouts` | Create campaign callout assets from 1-25 character text snippets. |
135
+ | `draft_structured_snippets` | Create campaign structured snippet assets using official header values and 3-10 snippet values. |
136
+ | `draft_image_assets` | Create campaign image assets from local PNG, JPEG, or GIF files. |
92
137
  | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
93
138
  | `add_negative_keywords` | Propose negative keywords to reduce wasted spend |
94
139
  | `pause_entity` | Pause a campaign, ad group, ad, or keyword |
@@ -143,6 +188,15 @@ AdLoop manages real ad spend, so safety is not optional.
143
188
 
144
189
  ### Quick Start (Recommended)
145
190
 
191
+ **Option A — Install from PyPI:**
192
+
193
+ ```bash
194
+ pip install adloop
195
+ adloop init
196
+ ```
197
+
198
+ **Option B — Install from source:**
199
+
146
200
  ```bash
147
201
  git clone https://github.com/kLOsk/adloop.git
148
202
  cd adloop
@@ -320,14 +374,6 @@ src/adloop/
320
374
  └── audit.py # Mutation audit logging
321
375
  ```
322
376
 
323
- ## Built From Real Usage
324
-
325
- AdLoop isn't a theoretical tool — it's built from running real Google Ads campaigns and hitting real problems. Every tool exists because of an actual situation: needing to diagnose a conversion drop without leaving the IDE, wanting to bulk-add negative keywords after seeing wasted spend in the search terms report, drafting ad variants that match a landing page the AI just helped rewrite.
326
-
327
- The cross-reference tools exist because we kept manually asking the AI to "get Ads data, then get GA4 data, then compare them" — so we automated the join. The Broad Match + Manual CPC safety rule exists because the AI once created that exact combination and wasted budget. The GDPR consent awareness exists because the AI kept diagnosing normal EU cookie rejection as broken tracking.
328
-
329
- The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **that's exactly the kind of feedback that shapes what gets built next.** Open an issue describing your situation — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
330
-
331
377
  ## Roadmap
332
378
 
333
379
  What's been shipped and what's next:
@@ -339,7 +385,7 @@ What's been shipped and what's next:
339
385
  - ~~Budget estimation via Keyword Planner~~ ✓
340
386
  - ~~Setup wizard (`adloop init`)~~ ✓
341
387
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
342
- - **PyPI package** — `pip install adloop`
388
+ - ~~PyPI package~~ — `pip install adloop`
343
389
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
344
390
  - **Video walkthrough**
345
391
 
@@ -1,29 +1,10 @@
1
- Metadata-Version: 2.3
2
- Name: adloop
3
- Version: 0.1.0
4
- Summary: MCP server connecting Google Ads + GA4 + codebase into one AI-driven feedback loop
5
- Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
- Author: Daniel Klose
7
- Author-email: Daniel Klose <info@daniel-klose.com>
8
- License: MIT
9
- Requires-Dist: fastmcp>=3.0.0
10
- Requires-Dist: google-ads>=29.0.0
11
- Requires-Dist: google-analytics-data>=0.20.0
12
- Requires-Dist: google-analytics-admin>=0.27.0
13
- Requires-Dist: google-auth-oauthlib>=1.0.0
14
- Requires-Dist: pyyaml>=6.0
15
- Requires-Dist: pytest>=8.0 ; extra == 'dev'
16
- Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
17
- Requires-Python: >=3.11
18
- Provides-Extra: dev
19
- Description-Content-Type: text/markdown
20
-
21
1
  <div align="center">
22
2
 
23
3
  # AdLoop
24
4
 
25
- **MCP server that connects Google Ads + Google Analytics (GA4) into one AI-driven feedback loop inside your IDE.**
5
+ **Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.**
26
6
 
7
+ [![PyPI](https://img.shields.io/pypi/v/adloop.svg)](https://pypi.org/project/adloop/)
27
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
28
9
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
29
10
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-8A2BE2.svg)](https://modelcontextprotocol.io)
@@ -31,25 +12,39 @@ Description-Content-Type: text/markdown
31
12
  [![GA4 Data API](https://img.shields.io/badge/GA4-Data%20API-E37400.svg?logo=google-analytics&logoColor=white)](https://developers.google.com/analytics/devguides/reporting/data/v1)
32
13
  [![GitHub stars](https://img.shields.io/github/stars/kLOsk/adloop?style=social)](https://github.com/kLOsk/adloop)
33
14
 
34
- *26 tools · Read + Write · Safety-first · GDPR-aware*
15
+ An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
35
16
 
36
- > "You built your product with AI. Now manage your ads the same way."
17
+ `pip install adloop`
37
18
 
38
19
  </div>
39
20
 
40
21
  ---
41
22
 
42
- ## Why
23
+ ## What It Solves
24
+
25
+ AdLoop exists because managing Google Ads alongside your code is a mess. These are the specific problems it handles:
26
+
27
+ - **"My conversions dropped and I don't know why."** AdLoop cross-references Ads clicks, GA4 sessions, and conversion events in one query. It detects whether the gap is from GDPR consent rejection, broken tracking, or an actual landing page problem — before you waste hours checking each dashboard separately.
28
+
29
+ - **"I'm wasting ad spend on irrelevant searches."** Pull your search terms report, identify the junk, and add negative keywords — all from a single conversation in your IDE. No context-switching to the Ads UI.
30
+
31
+ - **"Is my tracking even working?"** Compare the event names in your actual codebase against what GA4 is receiving. Find the mismatches: events you fire that GA4 never sees, events GA4 records that you didn't know about.
43
32
 
44
- Solo founders and small teams ship products fast with AI-assisted coding — but managing Google Ads still means switching between the Ads UI, GA4 dashboards, and your code editor. Whether you're running a SaaS, an e-commerce store, a local service, or anything else you drive traffic to with Google Ads the workflow is the same fragmented mess.
33
+ - **"I need to create ads but the Google Ads UI is hostile."** Draft responsive search ads, create campaigns, add keywords all through natural language. Every change shows a preview first. Nothing goes live without your explicit confirmation. New ads and campaigns start paused.
45
34
 
46
- AdLoop brings the entire **build ship market measure iterate** cycle into your IDE.
35
+ - **"My landing page gets paid traffic but nobody converts."** AdLoop joins your ad final URLs with GA4 page-level data. See which pages get clicks but no conversions, which have high bounce rates, and which ones are orphaned from any ad campaign.
47
36
 
48
- One MCP server gives your AI assistant access to both Google Analytics and Google Ads (read + write), with a safety layer that prevents accidental spend. Combined with your codebase context, it can do things no dashboard can like diagnosing why conversions dropped by cross-referencing ad traffic, analytics events, and your actual frontend code.
37
+ - **"I don't know if my EU consent setup is causing data gaps."** In Europe, 30-70% of users reject analytics cookies. AdLoop accounts for this automaticallyit won't diagnose a normal GDPR consent gap as broken tracking.
49
38
 
50
- ## What's Included 26 Tools
39
+ ## Built From Real Usage
40
+
41
+ Every tool exists because of an actual problem hit while running real Google Ads campaigns. The cross-reference tools exist because we kept manually asking the AI to "get Ads data, then get GA4 data, then compare them" — so we automated the join. The Broad Match + Manual CPC safety rule exists because the AI once created that exact combination and wasted budget. The GDPR consent awareness exists because the AI kept diagnosing normal EU cookie rejection as broken tracking.
42
+
43
+ The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **open an issue describing your situation** — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
51
44
 
52
- > **Quick start:** `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
45
+ ## All 33 Tools
46
+
47
+ > **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
53
48
 
54
49
  ### Diagnostics
55
50
 
@@ -107,8 +102,14 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
107
102
 
108
103
  | Tool | What It Does |
109
104
  |------|-------------|
110
- | `draft_campaign` | Create a full campaign structure — budget + campaign (PAUSED) + ad group + optional keywords. Validates bidding strategy, enforces budget caps, rejects unsafe BROAD match + Manual CPC combinations. |
105
+ | `draft_campaign` | Create a full campaign structure — budget + campaign (PAUSED) + ad group + optional keywords. Supports Search partners, display expansion, and `max_cpc` for either MANUAL_CPC initial ad-group bids or TARGET_SPEND (Maximize Clicks) CPC caps. |
106
+ | `update_campaign` | Modify existing campaign settings — bidding, budget, geo/language targeting, Search partners, display expansion, and TARGET_SPEND (Maximize Clicks) `max_cpc` caps. |
107
+ | `draft_ad_group` | Create a paused SEARCH_STANDARD ad group inside an existing campaign, with optional MANUAL_CPC `max_cpc`. |
108
+ | `update_ad_group` | Update an ad group name and/or MANUAL_CPC `max_cpc`. Use `pause_entity` / `enable_entity` for ad-group status changes. |
111
109
  | `draft_responsive_search_ad` | Create RSA preview (3-15 headlines ≤30 chars, 2-4 descriptions ≤90 chars). Warns if headline/description count is below best practice. |
110
+ | `draft_callouts` | Create campaign callout assets from 1-25 character text snippets. |
111
+ | `draft_structured_snippets` | Create campaign structured snippet assets using official header values and 3-10 snippet values. |
112
+ | `draft_image_assets` | Create campaign image assets from local PNG, JPEG, or GIF files. |
112
113
  | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
113
114
  | `add_negative_keywords` | Propose negative keywords to reduce wasted spend |
114
115
  | `pause_entity` | Pause a campaign, ad group, ad, or keyword |
@@ -163,6 +164,15 @@ AdLoop manages real ad spend, so safety is not optional.
163
164
 
164
165
  ### Quick Start (Recommended)
165
166
 
167
+ **Option A — Install from PyPI:**
168
+
169
+ ```bash
170
+ pip install adloop
171
+ adloop init
172
+ ```
173
+
174
+ **Option B — Install from source:**
175
+
166
176
  ```bash
167
177
  git clone https://github.com/kLOsk/adloop.git
168
178
  cd adloop
@@ -340,14 +350,6 @@ src/adloop/
340
350
  └── audit.py # Mutation audit logging
341
351
  ```
342
352
 
343
- ## Built From Real Usage
344
-
345
- AdLoop isn't a theoretical tool — it's built from running real Google Ads campaigns and hitting real problems. Every tool exists because of an actual situation: needing to diagnose a conversion drop without leaving the IDE, wanting to bulk-add negative keywords after seeing wasted spend in the search terms report, drafting ad variants that match a landing page the AI just helped rewrite.
346
-
347
- The cross-reference tools exist because we kept manually asking the AI to "get Ads data, then get GA4 data, then compare them" — so we automated the join. The Broad Match + Manual CPC safety rule exists because the AI once created that exact combination and wasted budget. The GDPR consent awareness exists because the AI kept diagnosing normal EU cookie rejection as broken tracking.
348
-
349
- The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **that's exactly the kind of feedback that shapes what gets built next.** Open an issue describing your situation — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
350
-
351
353
  ## Roadmap
352
354
 
353
355
  What's been shipped and what's next:
@@ -359,7 +361,7 @@ What's been shipped and what's next:
359
361
  - ~~Budget estimation via Keyword Planner~~ ✓
360
362
  - ~~Setup wizard (`adloop init`)~~ ✓
361
363
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
362
- - **PyPI package** — `pip install adloop`
364
+ - ~~PyPI package~~ — `pip install adloop`
363
365
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
364
366
  - **Video walkthrough**
365
367
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "MCP server connecting Google Ads + GA4 + codebase into one AI-driven feedback loop"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -18,6 +18,12 @@ dependencies = [
18
18
  "pyyaml>=6.0",
19
19
  ]
20
20
 
21
+ [project.urls]
22
+ Homepage = "https://github.com/kLOsk/adloop"
23
+ Repository = "https://github.com/kLOsk/adloop"
24
+ Issues = "https://github.com/kLOsk/adloop/issues"
25
+ Changelog = "https://github.com/kLOsk/adloop/releases"
26
+
21
27
  [project.scripts]
22
28
  adloop = "adloop:main"
23
29
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import sys
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.0"
6
6
 
7
7
 
8
8
  def main() -> None:
@@ -0,0 +1,79 @@
1
+ """Currency detection and formatting — auto-detect from Google Ads account."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from adloop.config import AdLoopConfig
9
+
10
+ _CURRENCY_SYMBOLS: dict[str, str] = {
11
+ "EUR": "\u20ac",
12
+ "USD": "$",
13
+ "GBP": "\u00a3",
14
+ "PLN": "z\u0142",
15
+ "CHF": "CHF",
16
+ "CZK": "K\u010d",
17
+ "SEK": "kr",
18
+ "NOK": "kr",
19
+ "DKK": "kr",
20
+ "HUF": "Ft",
21
+ "RON": "lei",
22
+ "BGN": "лв",
23
+ "HRK": "kn",
24
+ "TRY": "\u20ba",
25
+ "JPY": "\u00a5",
26
+ "CNY": "\u00a5",
27
+ "KRW": "\u20a9",
28
+ "INR": "\u20b9",
29
+ "BRL": "R$",
30
+ "AUD": "A$",
31
+ "CAD": "C$",
32
+ "NZD": "NZ$",
33
+ "MXN": "MX$",
34
+ "ARS": "ARS",
35
+ "ZAR": "R",
36
+ "RUB": "\u20bd",
37
+ "UAH": "\u20b4",
38
+ }
39
+
40
+ # Module-level cache: customer_id -> currency_code (one API call per session)
41
+ _cache: dict[str, str] = {}
42
+
43
+
44
+ def get_currency_code(config: AdLoopConfig, customer_id: str) -> str:
45
+ """Detect the account's currency via ``customer.currency_code``.
46
+
47
+ Result is cached per *customer_id* for the lifetime of the server process.
48
+ Falls back to ``"EUR"`` on any error.
49
+ """
50
+ from adloop.ads.client import normalize_customer_id
51
+
52
+ cid = normalize_customer_id(customer_id)
53
+ if cid in _cache:
54
+ return _cache[cid]
55
+
56
+ try:
57
+ from adloop.ads.gaql import execute_query
58
+
59
+ rows = execute_query(
60
+ config, customer_id, "SELECT customer.currency_code FROM customer LIMIT 1"
61
+ )
62
+ code = (rows[0].get("customer.currency_code", "EUR") if rows else "EUR") or "EUR"
63
+ except Exception:
64
+ code = "EUR"
65
+
66
+ _cache[cid] = code
67
+ return code
68
+
69
+
70
+ def format_currency(amount: float, currency_code: str) -> str:
71
+ """Format *amount* with the correct currency symbol/code.
72
+
73
+ Examples::
74
+
75
+ format_currency(42.50, "PLN") -> "42.50 PLN"
76
+ format_currency(42.50, "EUR") -> "42.50 EUR"
77
+ format_currency(42.50, "USD") -> "42.50 USD"
78
+ """
79
+ return f"{amount:.2f} {currency_code}"
@@ -63,6 +63,16 @@ def run_gaql(
63
63
  # ---------------------------------------------------------------------------
64
64
 
65
65
  _GAQL_ERROR_HINTS = {
66
+ "DEVELOPER_TOKEN_NOT_APPROVED": (
67
+ "Your Google Ads developer token is only approved for test accounts. "
68
+ "Apply for Basic or Standard access in the Google Ads API Center, "
69
+ "or use a test account."
70
+ ),
71
+ "DEVELOPER_TOKEN_INVALID": (
72
+ "Your Google Ads developer token is invalid. Update "
73
+ "`ads.developer_token` in `~/.adloop/config.yaml` using the token "
74
+ "from your manager account API Center."
75
+ ),
66
76
  "EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE": (
67
77
  "Fields used in ORDER BY or HAVING must also appear in the SELECT clause. "
68
78
  "Add the missing field to your SELECT."
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from adloop.ads.currency import get_currency_code
8
+
7
9
  if TYPE_CHECKING:
8
10
  from adloop.config import AdLoopConfig
9
11
 
@@ -57,7 +59,8 @@ def get_campaign_performance(
57
59
  """
58
60
 
59
61
  rows = execute_query(config, customer_id, query)
60
- _enrich_cost_fields(rows)
62
+ currency_code = get_currency_code(config, customer_id)
63
+ _enrich_cost_fields(rows, currency_code)
61
64
 
62
65
  return {"campaigns": rows, "total_campaigns": len(rows)}
63
66
 
@@ -75,7 +78,7 @@ def get_ad_performance(
75
78
  date_clause = _date_clause(date_range_start, date_range_end)
76
79
 
77
80
  query = f"""
78
- SELECT campaign.name, ad_group.name,
81
+ SELECT campaign.name, campaign.id, ad_group.name, ad_group.id,
79
82
  ad_group_ad.ad.id, ad_group_ad.ad.type,
80
83
  ad_group_ad.ad.responsive_search_ad.headlines,
81
84
  ad_group_ad.ad.responsive_search_ad.descriptions,
@@ -90,7 +93,8 @@ def get_ad_performance(
90
93
  """
91
94
 
92
95
  rows = execute_query(config, customer_id, query)
93
- _enrich_cost_fields(rows)
96
+ currency_code = get_currency_code(config, customer_id)
97
+ _enrich_cost_fields(rows, currency_code)
94
98
 
95
99
  return {"ads": rows, "total_ads": len(rows)}
96
100
 
@@ -122,7 +126,8 @@ def get_keyword_performance(
122
126
  """
123
127
 
124
128
  rows = execute_query(config, customer_id, query)
125
- _enrich_cost_fields(rows)
129
+ currency_code = get_currency_code(config, customer_id)
130
+ _enrich_cost_fields(rows, currency_code)
126
131
 
127
132
  return {"keywords": rows, "total_keywords": len(rows)}
128
133
 
@@ -176,7 +181,8 @@ def get_search_terms(
176
181
  """
177
182
 
178
183
  rows = execute_query(config, customer_id, query)
179
- _enrich_cost_fields(rows)
184
+ currency_code = get_currency_code(config, customer_id)
185
+ _enrich_cost_fields(rows, currency_code)
180
186
 
181
187
  return {"search_terms": rows, "total_search_terms": len(rows)}
182
188
 
@@ -223,7 +229,7 @@ def _date_clause(start: str, end: str) -> str:
223
229
  return "AND segments.date DURING LAST_30_DAYS"
224
230
 
225
231
 
226
- def _enrich_cost_fields(rows: list[dict]) -> None:
232
+ def _enrich_cost_fields(rows: list[dict], currency_code: str = "EUR") -> None:
227
233
  """Add human-readable cost and CPA fields computed from cost_micros."""
228
234
  for row in rows:
229
235
  cost_micros = row.get("metrics.cost_micros", 0) or 0
@@ -235,4 +241,6 @@ def _enrich_cost_fields(rows: list[dict]) -> None:
235
241
 
236
242
  avg_cpc_micros = row.get("metrics.average_cpc", 0) or 0
237
243
  if avg_cpc_micros:
238
- row["metrics.average_cpc_eur"] = round(avg_cpc_micros / 1_000_000, 2)
244
+ row["metrics.average_cpc_amount"] = round(avg_cpc_micros / 1_000_000, 2)
245
+
246
+ row["metrics.currency"] = currency_code