133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""
|
|||
|
|
api/main.py
|
||
|
|
REST API Service for HIFFI Hip-Hop 500 Multi-Platform Intelligence.
|
||
|
|
Secured with header-based auth secret (X-Auth-Secret).
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import sys
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from dotenv import load_dotenv
|
||
|
|
from fastapi import FastAPI, Header, HTTPException, Query, Security, status
|
||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
if str(PROJECT_ROOT) not in sys.path:
|
||
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
|
|
||
|
|
load_dotenv(PROJECT_ROOT / ".env")
|
||
|
|
|
||
|
|
AUTH_SECRET = os.getenv("AUTH_SECRET", "hiffi_live_secret_key_prod_2026").strip()
|
||
|
|
DATA_FILE = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
|
||
|
|
|
||
|
|
app = FastAPI(
|
||
|
|
title="HIFFI Artist Intelligence API",
|
||
|
|
version="1.0.0",
|
||
|
|
description="Programmatic query and ranking endpoints for the HIFFI Hip-Hop 500 cohort.",
|
||
|
|
)
|
||
|
|
|
||
|
|
app.add_middleware(
|
||
|
|
CORSMiddleware,
|
||
|
|
allow_origins=["*"],
|
||
|
|
allow_credentials=True,
|
||
|
|
allow_methods=["*"],
|
||
|
|
allow_headers=["*"],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def verify_auth_secret(x_auth_secret: Optional[str] = Header(None)):
|
||
|
|
"""Validates the auth secret passed in the request header."""
|
||
|
|
if not x_auth_secret or x_auth_secret != AUTH_SECRET:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
|
|
detail="Unauthorized: Invalid or missing X-Auth-Secret header.",
|
||
|
|
)
|
||
|
|
return x_auth_secret
|
||
|
|
|
||
|
|
|
||
|
|
def get_dataset() -> pd.DataFrame:
|
||
|
|
"""Loads the current master cohort dataset."""
|
||
|
|
if not DATA_FILE.exists():
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||
|
|
detail="Cohort dataset not found. Please run run_pipeline.py first.",
|
||
|
|
)
|
||
|
|
return pd.read_csv(DATA_FILE)
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/health", tags=["System"])
|
||
|
|
def health_check():
|
||
|
|
"""Public health probe."""
|
||
|
|
return {"status": "HEALTHY", "dataset_exists": DATA_FILE.exists()}
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/api/v1/cohort", tags=["Rankings"])
|
||
|
|
def get_cohort_rankings(
|
||
|
|
tier: Optional[int] = Query(None, ge=0, le=3, description="Filter by platform evidence tier (0-3)"),
|
||
|
|
limit: int = Query(50, ge=1, le=500, description="Max records to return"),
|
||
|
|
offset: int = Query(0, ge=0, description="Offset for pagination"),
|
||
|
|
auth: str = Security(verify_auth_secret),
|
||
|
|
):
|
||
|
|
"""Fetches published cohort rankings with pagination and tier filtering."""
|
||
|
|
df = get_dataset()
|
||
|
|
|
||
|
|
if tier is not None and "platform_evidence_tier" in df.columns:
|
||
|
|
df = df[df["platform_evidence_tier"] == tier]
|
||
|
|
|
||
|
|
if "hiffi_xp_published_rank" in df.columns:
|
||
|
|
df = df.sort_values(by="hiffi_xp_published_rank", ascending=True)
|
||
|
|
|
||
|
|
paginated = df.iloc[offset : offset + limit]
|
||
|
|
|
||
|
|
# Safe replacement of NaN/inf with None for valid JSON serialization
|
||
|
|
clean_df = paginated.replace({float('nan'): None, float('inf'): None, float('-inf'): None})
|
||
|
|
records = clean_df.to_dict(orient="records")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"total": len(df),
|
||
|
|
"limit": limit,
|
||
|
|
"offset": offset,
|
||
|
|
"results": records,
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/api/v1/artists/{artist_name}", tags=["Artists"])
|
||
|
|
def get_artist_dossier(
|
||
|
|
artist_name: str,
|
||
|
|
auth: str = Security(verify_auth_secret),
|
||
|
|
):
|
||
|
|
"""Retrieves full telemetry, evidence tiers, and ranks for a single artist."""
|
||
|
|
df = get_dataset()
|
||
|
|
match = df[df["name"].str.strip().str.lower() == artist_name.strip().lower()]
|
||
|
|
|
||
|
|
if match.empty:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
||
|
|
detail=f"Artist '{artist_name}' not found in cohort.",
|
||
|
|
)
|
||
|
|
|
||
|
|
row = match.iloc[0].where(pd.notna(match.iloc[0]), None).to_dict()
|
||
|
|
return {"status": "SUCCESS", "artist": row}
|
||
|
|
|
||
|
|
|
||
|
|
@app.post("/api/v1/pipeline/run", tags=["Pipeline"])
|
||
|
|
def trigger_pipeline_run(auth: str = Security(verify_auth_secret)):
|
||
|
|
"""Triggers deterministic re-scoring, anomaly detection, and publication gating."""
|
||
|
|
from core.ranking_engine import HIFFIRankingEngine
|
||
|
|
|
||
|
|
df = get_dataset()
|
||
|
|
engine = HIFFIRankingEngine()
|
||
|
|
|
||
|
|
scored = engine.calculate_hiffi_xp(df)
|
||
|
|
flagged = engine.detect_metric_anomalies(scored)
|
||
|
|
gated, audit = engine.apply_publication_eligibility_gates(flagged, return_metadata=True)
|
||
|
|
|
||
|
|
gated.to_csv(DATA_FILE, index=False)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "SUCCESS",
|
||
|
|
"message": "Pipeline recalculated and dataset refreshed.",
|
||
|
|
"shortfall_audit": audit,
|
||
|
|
}
|