Files
moreminimore-service-system/scripts/mm_gsc.py
Kunthawat Greethong 7b5af16f6d Initial commit: moreminimore-service content pipeline
- 9 mm* skills (orchestrator, article, social, publish, analytics)
- 6 dependency skills (content-writer, geo-optimizer, etc.)
- 3 analytics scripts (GSC, Google Ads, Meta Ads)
- Config template + setup guide
- SOUL-MM.md persona extension
- OrbitOS integration reference
2026-07-02 10:01:53 +07:00

320 lines
10 KiB
Python

#!/usr/bin/env python3
"""
GSC Analytics for moreminimore-service.
Reads clients-config.json, pulls GSC data using service account key.
Usage:
python3 mm_gsc.py --client moreminimore --days 90
python3 mm_gsc.py --client moreminimore --days 28
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from datetime import date, timedelta
def load_config():
"""Load clients-config.json"""
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
if not os.path.exists(config_path):
print(f"ERROR: Config not found at {config_path}", file=sys.stderr)
sys.exit(1)
with open(config_path) as f:
return json.load(f)
def find_client(config, client_id):
"""Find client by ID"""
for client in config.get("clients", []):
if client["id"] == client_id:
return client
return None
def get_gsc_property(client):
"""Get GSC property from client config"""
analytics = client.get("analytics", {})
gsc_property = analytics.get("gsc_property", "")
if not gsc_property:
print(f"ERROR: No gsc_property set for client '{client['id']}'", file=sys.stderr)
sys.exit(1)
return gsc_property
def get_access_token(key_path):
"""Get access token from service account key using openssl for JWT signing"""
import subprocess
import base64
import time
if not os.path.exists(key_path):
print(f"ERROR: Service account key not found: {key_path}", file=sys.stderr)
sys.exit(1)
try:
with open(key_path) as f:
key_data = json.load(f)
# JWT header
header = {"alg": "RS256", "typ": "JWT"}
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
# JWT payload
now = int(time.time())
payload = {
"iss": key_data["client_email"],
"scope": "https://www.googleapis.com/auth/webmasters.readonly",
"aud": "https://oauth2.googleapis.com/token",
"iat": now,
"exp": now + 3600,
}
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
# Sign with openssl
signing_input = f"{header_b64}.{payload_b64}"
# Write private key to temp file
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
kf.write(key_data["private_key"])
key_file = kf.name
try:
# Sign with openssl
result = subprocess.run(
["openssl", "dgst", "-sha256", "-sign", key_file],
input=signing_input.encode(),
capture_output=True,
timeout=10,
)
if result.returncode != 0:
print(f"ERROR: openssl signing failed: {result.stderr.decode()}", file=sys.stderr)
sys.exit(1)
signature_b64 = base64.urlsafe_b64encode(result.stdout).decode().rstrip('=')
signed_jwt = f"{signing_input}.{signature_b64}"
finally:
os.unlink(key_file)
# Exchange JWT for access token
token_url = "https://oauth2.googleapis.com/token"
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt,
}).encode()
req = urllib.request.Request(token_url, data=data, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
with urllib.request.urlopen(req, timeout=15) as resp:
token_data = json.loads(resp.read().decode())
return token_data["access_token"]
except Exception as e:
print(f"ERROR: Failed to get access token: {e}", file=sys.stderr)
print(f" Key file: {key_path}", file=sys.stderr)
sys.exit(1)
def gsc_query(site_property, access_token, request_body):
"""Execute a GSC API query"""
# URL encode the site property
import urllib.parse
site_encoded = urllib.parse.quote(site_property, safe='')
url = f"https://searchconsole.googleapis.com/webmasters/v3/sites/{site_encoded}/searchAnalytics/query"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
body = json.dumps(request_body).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f"ERROR: GSC API returned {e.code}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body[:500], file=sys.stderr)
sys.exit(1)
def get_top_queries(site_property, access_token, days=28):
"""Get top queries"""
end_date = date.today() - timedelta(days=3) # GSC data lags ~3 days
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["query"],
"rowLimit": 50,
"type": "web",
})
return result.get("rows", [])
def get_top_pages(site_property, access_token, days=28):
"""Get top pages"""
end_date = date.today() - timedelta(days=3)
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["page"],
"rowLimit": 50,
"type": "web",
})
return result.get("rows", [])
def get_summary(site_property, access_token, days=28):
"""Get overall summary"""
end_date = date.today() - timedelta(days=3)
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["date"],
"type": "web",
})
rows = result.get("rows", [])
total_clicks = sum(r.get("clicks", 0) for r in rows)
total_impressions = sum(r.get("impressions", 0) for r in rows)
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
avg_position = sum(r.get("position", 0) * r.get("impressions", 0) for r in rows) / total_impressions if total_impressions > 0 else 0
return {
"clicks": total_clicks,
"impressions": total_impressions,
"ctr": avg_ctr,
"position": avg_position,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
}
def display_summary(summary):
"""Display summary"""
print(f"📊 Summary ({summary['start_date']} to {summary['end_date']})")
print("-" * 60)
print(f" Clicks: {summary['clicks']:,}")
print(f" Impressions: {summary['impressions']:,}")
print(f" CTR: {summary['ctr']:.2f}%")
print(f" Avg Position: {summary['position']:.1f}")
def display_queries(rows):
"""Display top queries"""
if not rows:
print("\nไม่พบ queries")
return
print(f"\n🔑 Top Queries ({len(rows)})")
print("-" * 90)
print(f"{'Query':<45} {'Clicks':>10} {'Impressions':>12} {'CTR':>8} {'Position':>10}")
print("-" * 90)
for row in rows[:30]:
keys = row.get("keys", [])
query = keys[0] if keys else ""
ctr = row.get("ctr", 0) * 100
print(f"{query[:43]:<45} {row.get('clicks', 0):>10,} "
f"{row.get('impressions', 0):>12,} "
f"{ctr:>7.2f}% "
f"{row.get('position', 0):>10.1f}")
def display_pages(rows):
"""Display top pages"""
if not rows:
print("\nไม่พบ pages")
return
print(f"\n📄 Top Pages ({len(rows)})")
print("-" * 90)
print(f"{'Page':<55} {'Clicks':>10} {'Impressions':>12} {'CTR':>8}")
print("-" * 90)
for row in rows[:20]:
keys = row.get("keys", [])
page = keys[0] if keys else ""
ctr = row.get("ctr", 0) * 100
print(f"{page[:53]:<55} {row.get('clicks', 0):>10,} "
f"{row.get('impressions', 0):>12,} "
f"{ctr:>7.2f}%")
def main():
parser = argparse.ArgumentParser(description="GSC Analytics for moreminimore-service")
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
parser.add_argument("--days", type=int, default=90, help="Number of days to analyze (default: 90)")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
args = parser.parse_args()
# Load config
config = load_config()
# Find client
client = find_client(config, args.client)
if not client:
print(f"ERROR: Client '{args.client}' not found in config", file=sys.stderr)
print(f"Available clients: {[c['id'] for c in config.get('clients', [])]}", file=sys.stderr)
sys.exit(1)
# Get GSC property
gsc_property = get_gsc_property(client)
# Get service account key
key_path = config.get("global", {}).get("google", {}).get("service_account_key", "")
if not key_path:
print("ERROR: No service_account_key in global.google config", file=sys.stderr)
sys.exit(1)
print(f"📊 GSC Analytics for: {client['display_name']}")
print(f" Property: {gsc_property}")
print(f" Period: {args.days} days")
print()
# Get access token
access_token = get_access_token(os.path.expanduser(key_path))
# Get data
summary = get_summary(gsc_property, access_token, args.days)
queries = get_top_queries(gsc_property, access_token, args.days)
pages = get_top_pages(gsc_property, access_token, args.days)
if args.json:
result = {
"summary": summary,
"queries": queries,
"pages": pages,
}
print(json.dumps(result, indent=2))
else:
display_summary(summary)
display_queries(queries)
display_pages(pages)
if __name__ == "__main__":
main()