Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion google-ads/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Google Ads",
"version": "2.0.0",
"version": "2.0.1",
"description": "Full CRUD operations for Google Ads API including campaigns, ad groups, ads, keywords, and Keyword Planner functionality.",
"entry_point": "google_ads.py",
"supports_billing": true,
Expand Down
149 changes: 75 additions & 74 deletions google-ads/google_ads.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion google-ads/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
autohive-integrations-sdk~=1.0.2
autohive-integrations-sdk~=2.0.0
google-ads~=30.0
python-dotenv
proto-plus
211 changes: 211 additions & 0 deletions google-ads/run_live_readonly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""
Quick live test using a short-lived access token (no refresh token needed).

Usage:
Set the variables in the CONFIG block below, then run:
python google-ads/run_live_readonly.py
"""

import os
import sys
import asyncio
import importlib
from unittest.mock import MagicMock, AsyncMock

# ============================================================================
# CONFIG — fill these in before running
# ============================================================================
ACCESS_TOKEN = os.environ.get("GOOGLE_ADS_ACCESS_TOKEN", "")
DEVELOPER_TOKEN = os.environ.get("ADWORDS_DEVELOPER_TOKEN", "")
LOGIN_CUSTOMER_ID = os.environ.get("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "") # MCC account ID
CUSTOMER_ID = os.environ.get("GOOGLE_ADS_CUSTOMER_ID", "") # client account ID
# ============================================================================

if not all([ACCESS_TOKEN, DEVELOPER_TOKEN, LOGIN_CUSTOMER_ID, CUSTOMER_ID]):
print("Missing required config. Set these env vars:")
print(" GOOGLE_ADS_ACCESS_TOKEN")
print(" ADWORDS_DEVELOPER_TOKEN")
print(" GOOGLE_ADS_LOGIN_CUSTOMER_ID (MCC/manager account ID)")
print(" GOOGLE_ADS_CUSTOMER_ID (client account ID to query)")
sys.exit(1)

# Set placeholder env vars so google_ads.py module-level checks pass
os.environ.setdefault("ADWORDS_DEVELOPER_TOKEN", DEVELOPER_TOKEN)
os.environ.setdefault("ADWORDS_CLIENT_ID", "placeholder") # nosec B105
os.environ.setdefault("ADWORDS_CLIENT_SECRET", "placeholder") # nosec B105

_parent = os.path.abspath(os.path.join(os.path.dirname(__file__)))
_deps = os.path.abspath(os.path.join(os.path.dirname(__file__), "dependencies"))
sys.path.insert(0, _parent)
sys.path.insert(0, _deps)

_spec = importlib.util.spec_from_file_location("google_ads_mod", os.path.join(_parent, "google_ads.py"))
_mod = importlib.util.module_from_spec(_spec)

# Patch _get_google_ads_client to use access_token directly before exec
from google.ads.googleads.client import GoogleAdsClient
from google.oauth2.credentials import Credentials

def _client_from_access_token(refresh_token: str, login_customer_id=None):
credentials = Credentials(token=ACCESS_TOKEN)
kwargs = {
"credentials": credentials,
"developer_token": DEVELOPER_TOKEN,
"use_proto_plus": True,
}
if login_customer_id:
kwargs["login_customer_id"] = login_customer_id
return GoogleAdsClient(**kwargs)

_spec.loader.exec_module(_mod)
_mod._get_google_ads_client = _client_from_access_token

google_ads = _mod.google_ads

from autohive_integrations_sdk.integration import ResultType # noqa: E402

ctx = MagicMock(name="ExecutionContext")
ctx.fetch = AsyncMock()
ctx.auth = {"credentials": {"refresh_token": "unused-access-token-flow"}} # nosec B105

base = {"login_customer_id": LOGIN_CUSTOMER_ID, "customer_id": CUSTOMER_ID}

PASS = "✓"
FAIL = "✗"
SKIP = "—"


async def run():
results = []

async def check(label, coro):
try:
result = await coro
if result.type == ResultType.ACTION:
print(f" {PASS} {label}")
results.append((label, True, None))
return result
else:
msg = getattr(result.result, "message", None) or str(result.result)
print(f" {FAIL} {label}: {result.type.value} — {msg[:120]}")
results.append((label, False, msg))
return result
except Exception as e:
print(f" {FAIL} {label}: EXCEPTION — {e}")
results.append((label, False, str(e)))
return None

print("\n── get_accessible_accounts ──────────────────────────────")
r = await check(
"returns accounts list",
google_ads.execute_action("get_accessible_accounts", {}, ctx),
)
if r and r.type == ResultType.ACTION:
accounts = r.result.data.get("accounts", [])
print(f" → {len(accounts)} account(s) found")
for a in accounts[:5]:
print(f" • {a.get('customer_id')} — {a.get('descriptive_name')}")

print("\n── retrieve_campaign_metrics ────────────────────────────")
r = await check(
"last 7 days",
google_ads.execute_action(
"retrieve_campaign_metrics",
{**base, "date_ranges": ["last 7 days"]},
ctx,
),
)
if r and r.type == ResultType.ACTION:
entries = r.result.data.get("results", [{}])[0].get("data", [])
print(f" → {len(entries)} campaign(s)")
for c in entries[:3]:
print(f" • {c.get('Campaign')} — clicks: {c.get('Clicks')}, cost: {c.get('Cost')}")

print("\n── retrieve_ad_group_metrics ────────────────────────────")
r = await check(
"last 7 days",
google_ads.execute_action(
"retrieve_ad_group_metrics",
{**base, "date_ranges": ["last 7 days"]},
ctx,
),
)
if r and r.type == ResultType.ACTION:
entries = r.result.data.get("results", [{}])[0].get("data", [])
print(f" → {len(entries)} ad group(s)")

print("\n── retrieve_ad_metrics ──────────────────────────────────")
r = await check(
"last 7 days",
google_ads.execute_action(
"retrieve_ad_metrics",
{**base, "date_ranges": ["last 7 days"]},
ctx,
),
)
if r and r.type == ResultType.ACTION:
entries = r.result.data.get("results", [{}])[0].get("data", [])
print(f" → {len(entries)} ad(s)")

print("\n── retrieve_search_terms ────────────────────────────────")
await check(
"last 7 days",
google_ads.execute_action(
"retrieve_search_terms",
{**base, "date_ranges": ["last 7 days"]},
ctx,
),
)

print("\n── get_active_ad_urls ───────────────────────────────────")
r = await check(
"all active ads",
google_ads.execute_action("get_active_ad_urls", base, ctx),
)
if r and r.type == ResultType.ACTION:
print(f" → {r.result.data.get('total_count', 0)} active ad(s)")

print("\n── generate_keyword_ideas ───────────────────────────────")
r = await check(
"seed: digital marketing",
google_ads.execute_action(
"generate_keyword_ideas",
{**base, "seed_keywords": ["digital marketing"]},
ctx,
),
)
if r and r.type == ResultType.ACTION:
ideas = r.result.data.get("keyword_ideas", [])
print(f" → {len(ideas)} idea(s)")
for i in ideas[:3]:
print(f" • {i.get('keyword')} — {i.get('avg_monthly_searches')} searches/mo, comp: {i.get('competition')}")

print("\n── generate_keyword_historical_metrics ──────────────────")
r = await check(
"keywords: [digital marketing, seo]",
google_ads.execute_action(
"generate_keyword_historical_metrics",
{**base, "keywords": ["digital marketing", "seo"]},
ctx,
),
)
if r and r.type == ResultType.ACTION:
metrics = r.result.data.get("keyword_metrics", [])
print(f" → {len(metrics)} keyword(s)")
for m in metrics[:3]:
print(f" • {m.get('keyword')} — avg: {m.get('avg_monthly_searches')}, comp: {m.get('competition')}")

# Summary
total = len(results)
passed = sum(1 for _, ok, _ in results if ok)
print(f"\n{'─'*56}")
print(f" {passed}/{total} passed")
if passed < total:
print("\n Failures:")
for label, ok, err in results:
if not ok:
print(f" {FAIL} {label}: {err[:100] if err else 'unknown'}")
print()


asyncio.run(run())
5 changes: 5 additions & 0 deletions google-ads/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import os

# Allow imports to work when pytest runs from repo root
sys.path.insert(0, os.path.dirname(__file__))
6 changes: 0 additions & 6 deletions google-ads/tests/context.py

This file was deleted.

Loading
Loading