Add initial work from Codex

This commit is contained in:
2026-03-20 15:13:33 +01:00
parent 19771ddd37
commit adb5c1a439
48 changed files with 7054 additions and 16 deletions

View File

@@ -0,0 +1,260 @@
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from contextvars import ContextVar
import httpx
import pandas as pd
from fastapi import Depends, FastAPI, Query, Request, Response
from app.core.config import settings
from app.core.otel import (
TelemetryProviders,
configure_otel,
instrument_fastapi,
instrument_httpx_clients,
shutdown_otel,
)
from app.core.security import InternalPrincipal, require_internal_principal
from app.services.analytics_service import AnalyticsService
from microservices.common.http import current_trace_headers, with_internal_service_token
logging.basicConfig(level=settings.log_level)
LOGGER = logging.getLogger(__name__)
FORWARD_HEADERS: ContextVar[dict[str, str]] = ContextVar("forward_headers", default={})
class QueryWarehouseClient:
def __init__(self, client: httpx.Client, query_service_url: str) -> None:
self.client = client
self.query_service_url = query_service_url.rstrip("/")
def _fetch(self, path: str) -> pd.DataFrame:
response = self.client.get(
f"{self.query_service_url}{path}",
headers=FORWARD_HEADERS.get(),
timeout=settings.request_timeout_seconds,
)
response.raise_for_status()
return pd.DataFrame(response.json())
def fetch_daily_sales(self) -> pd.DataFrame:
return self._fetch("/internal/daily-sales")
def fetch_product_performance(self) -> pd.DataFrame:
return self._fetch("/internal/product-performance")
def fetch_customer_performance(self) -> pd.DataFrame:
return self._fetch("/internal/customer-performance")
class PersistenceProxy:
def __init__(self, client: httpx.Client, persistence_service_url: str) -> None:
self.client = client
self.persistence_service_url = persistence_service_url.rstrip("/")
def _post(self, path: str, payload: dict) -> None:
response = self.client.post(
f"{self.persistence_service_url}{path}",
headers=FORWARD_HEADERS.get(),
json=payload,
timeout=settings.request_timeout_seconds,
)
response.raise_for_status()
def record_forecast_run(
self,
*,
horizon_days: int,
payload: list[dict],
trigger_source: str,
trace_id: str | None,
span_id: str | None,
) -> None:
self._post(
"/internal/forecast-runs",
{
"horizon_days": horizon_days,
"payload": payload,
"trigger_source": trigger_source,
"trace_id": trace_id,
"span_id": span_id,
},
)
def record_ranking_run(
self,
*,
top_n: int,
payload: list[dict],
trigger_source: str,
trace_id: str | None,
span_id: str | None,
) -> None:
self._post(
"/internal/ranking-runs",
{
"top_n": top_n,
"payload": payload,
"trigger_source": trigger_source,
"trace_id": trace_id,
"span_id": span_id,
},
)
def record_recommendation_run(
self,
*,
payload: list[dict],
trigger_source: str,
trace_id: str | None,
span_id: str | None,
) -> None:
self._post(
"/internal/recommendation-runs",
{
"payload": payload,
"trigger_source": trigger_source,
"trace_id": trace_id,
"span_id": span_id,
},
)
@asynccontextmanager
async def lifespan(app: FastAPI):
telemetry: TelemetryProviders = configure_otel(settings)
instrument_httpx_clients()
http_client = httpx.Client()
warehouse_client = QueryWarehouseClient(http_client, settings.query_service_url)
persistence_proxy = PersistenceProxy(http_client, settings.persistence_service_url)
app.state.http_client = http_client
app.state.analytics = AnalyticsService(warehouse_client, persistence_proxy)
LOGGER.info("Analytics service ready")
yield
http_client.close()
shutdown_otel(telemetry)
app = FastAPI(title="analytics-service", version="0.1.0", lifespan=lifespan)
instrument_fastapi(app)
def _analytics() -> AnalyticsService:
return app.state.analytics
def _with_request_headers(request: Request):
headers = current_trace_headers()
incoming_internal = request.headers.get("x-internal-service-token")
if incoming_internal:
headers = with_internal_service_token(headers, incoming_internal)
token = FORWARD_HEADERS.set(headers)
return token
@app.get("/internal/health")
def health(request: Request, response: Response) -> dict:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return {"status": "ok", "service": "analytics-service"}
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/kpis")
def kpis(
request: Request,
response: Response,
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> dict:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return _analytics().get_kpis()
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/history")
def history(
request: Request,
response: Response,
days_back: int = Query(default=settings.default_history_days, ge=30, le=1460),
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> list[dict]:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return _analytics().get_history_points(days_back=days_back)
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/forecasts")
def forecasts(
request: Request,
response: Response,
days: int = Query(default=settings.forecast_horizon_days, ge=7, le=180),
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> list[dict]:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return _analytics().get_forecast(
horizon_days=days, trigger_source="analytics.api.forecasts", persist=True
)
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/rankings")
def rankings(
request: Request,
response: Response,
top_n: int = Query(default=settings.ranking_default_top_n, ge=3, le=100),
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> list[dict]:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return _analytics().get_rankings(
top_n=top_n, trigger_source="analytics.api.rankings", persist=True
)
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/recommendations")
def recommendations(
request: Request,
response: Response,
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> list[dict]:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
return _analytics().get_recommendations(
trigger_source="analytics.api.recommendations", persist=True
)
finally:
FORWARD_HEADERS.reset(token)
@app.get("/internal/dashboard")
def dashboard(
request: Request,
response: Response,
_auth: InternalPrincipal = Depends(require_internal_principal),
) -> dict:
token = _with_request_headers(request)
try:
response.headers.update(current_trace_headers())
snapshot = _analytics().get_dashboard()
return snapshot.__dict__
finally:
FORWARD_HEADERS.reset(token)