mcp: api.py — fix column names to match live ClickHouse schema

This commit is contained in:
2026-05-09 01:32:49 +00:00
parent 3483c9d9bc
commit bbad33a4f1
+176
View File
@@ -0,0 +1,176 @@
"""
Manar REST API — FastAPI on port 8092
Columns: brand_id, platform, account_id, campaign_id, campaign_name,
adgroup_id, adgroup_name, hour, date, impressions, clicks,
cost_micros, conversions, conv_value, video_views
"""
import os
import threading
import logging
from datetime import date, timedelta
import clickhouse_connect
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
log = logging.getLogger("manar-api")
CH_HOST = os.environ.get("CLICKHOUSE_HOST", "clickhouse")
CH_PORT = int(os.environ.get("CLICKHOUSE_PORT", 8123))
CH_DB = os.environ.get("CLICKHOUSE_DB", "manar")
CH_USER = os.environ.get("CLICKHOUSE_USER", "default")
CH_PASS = os.environ.get("CLICKHOUSE_PASSWORD", "")
API_PORT = int(os.environ.get("API_PORT", 8092))
app = FastAPI(title="Manar REST API", version="0.3.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"])
def get_ch():
return clickhouse_connect.get_client(
host=CH_HOST, port=CH_PORT, database=CH_DB,
username=CH_USER, password=CH_PASS
)
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/{brand}/summary")
def summary(brand: str, days: int = Query(30, ge=1, le=365)):
try:
ch = get_ch()
result = ch.query(f"""
SELECT period,
sum(impressions) AS impressions,
sum(clicks) AS clicks,
round(sum(cost_micros)/1e6, 2) AS cost,
round(sum(conversions), 1) AS conversions,
round(sum(conv_value), 2) AS conv_value,
round(if(sum(impressions)>0, sum(clicks)/sum(impressions)*100, 0), 2) AS ctr_pct,
round(if(sum(clicks)>0, sum(cost_micros)/1e6/sum(clicks), 0), 2) AS cpc,
round(if(sum(cost_micros)>0, sum(conv_value)/(sum(cost_micros)/1e6), 0), 2) AS roas
FROM (
SELECT if(date >= today()-{days}, 'current', 'prior') AS period,
impressions, clicks, cost_micros, conversions, conv_value
FROM manar.ad_metrics_hourly
WHERE brand_id='{brand}' AND date >= today()-{days*2}
) GROUP BY period
""")
data = {}
for row in result.result_rows:
d = dict(zip(result.column_names, row))
data[d.pop("period")] = d
current = data.get("current", {})
prior = data.get("prior", {})
def pct(a, b):
try: return round((a - b) / b * 100, 1) if b else None
except: return None
return {
"brand": brand, "days": days,
"current": current, "prior": prior,
"changes": {k: pct(current.get(k, 0), prior.get(k, 0))
for k in ["impressions", "clicks", "cost", "conversions", "roas", "cpc"]}
}
except Exception as e:
log.error(f"summary {brand}: {e}")
return {"brand": brand, "days": days, "current": {}, "prior": {}, "changes": {}}
@app.get("/api/{brand}/campaigns")
def campaigns(brand: str, days: int = Query(30, ge=1, le=365)):
try:
ch = get_ch()
today = date.today()
date_from = today - timedelta(days=days)
result = ch.query(f"""
SELECT campaign_id, campaign_name,
sum(impressions) AS impressions, sum(clicks) AS clicks,
round(sum(cost_micros)/1e6, 2) AS cost,
round(sum(conversions), 2) AS conversions,
round(sum(conv_value), 2) AS conv_value,
round(if(sum(impressions)>0, sum(clicks)/sum(impressions), 0), 4) AS ctr,
round(if(sum(clicks)>0, sum(cost_micros)/1e6/sum(clicks), 0), 2) AS cpc,
round(if(sum(cost_micros)>0, sum(conv_value)/(sum(cost_micros)/1e6), 0), 2) AS roas
FROM manar.ad_metrics_hourly
WHERE brand_id='{brand}' AND date>='{date_from}' AND date<='{today}'
GROUP BY campaign_id, campaign_name ORDER BY cost DESC LIMIT 50
""")
return [dict(zip(result.column_names, r)) for r in result.result_rows]
except Exception as e:
log.error(f"campaigns {brand}: {e}")
return []
@app.get("/api/{brand}/trend")
def trend(brand: str, days: int = Query(30, ge=1, le=365)):
try:
ch = get_ch()
today = date.today()
date_from = today - timedelta(days=days)
result = ch.query(f"""
SELECT toString(date) AS date,
sum(impressions) AS impressions, sum(clicks) AS clicks,
round(sum(cost_micros)/1e6, 2) AS cost,
round(sum(conversions), 2) AS conversions
FROM manar.ad_metrics_hourly
WHERE brand_id='{brand}' AND date>='{date_from}' AND date<='{today}'
GROUP BY date ORDER BY date ASC
""")
return [dict(zip(result.column_names, r)) for r in result.result_rows]
except Exception as e:
log.error(f"trend {brand}: {e}")
return []
@app.get("/api/{brand}/search-terms")
def search_terms(brand: str, days: int = Query(30, ge=1, le=365), limit: int = Query(50, ge=1, le=200)):
try:
ch = get_ch()
today = date.today()
date_from = today - timedelta(days=days)
result = ch.query(f"""
SELECT search_term,
sum(impressions) AS impressions, sum(clicks) AS clicks,
round(sum(cost_micros)/1e6, 2) AS cost,
round(sum(conversions), 2) AS conversions,
round(if(sum(clicks)>0, sum(cost_micros)/1e6/sum(clicks), 0), 2) AS cpc
FROM manar.search_term_performance
WHERE brand_id='{brand}' AND date>='{date_from}' AND date<='{today}'
GROUP BY search_term ORDER BY cost DESC LIMIT {limit}
""")
return [dict(zip(result.column_names, r)) for r in result.result_rows]
except Exception as e:
log.error(f"search-terms {brand}: {e}")
return []
@app.get("/api/{brand}/anomalies")
def anomalies(brand: str):
try:
ch = get_ch()
result = ch.query(f"""
SELECT campaign_id, metric, expected_value, actual_value,
deviation_pct, severity, toString(detected_at) AS detected_at
FROM manar.anomaly_events
WHERE brand_id='{brand}' AND resolved_at IS NULL
ORDER BY detected_at DESC LIMIT 50
""")
return [dict(zip(result.column_names, r)) for r in result.result_rows]
except Exception as e:
log.error(f"anomalies {brand}: {e}")
return []
def start_api_thread():
def run():
uvicorn.run(app, host="0.0.0.0", port=API_PORT, log_level="warning")
t = threading.Thread(target=run, daemon=True)
t.start()
log.info(f"Manar REST API on :{API_PORT}")