from __future__ import annotations import logging from contextlib import asynccontextmanager import pandas as pd from fastapi import Depends, FastAPI, Response from app.core.config import settings from app.core.otel import ( TelemetryProviders, configure_otel, instrument_fastapi, instrument_sqlalchemy_engines, shutdown_otel, ) from app.core.security import InternalPrincipal, require_internal_principal from app.db.engine import create_warehouse_engines, dispose_engines from app.services.warehouse_service import ReadOnlyWarehouseClient from microservices.common.http import current_trace_headers logging.basicConfig(level=settings.log_level) LOGGER = logging.getLogger(__name__) def _frame_to_rows(df: pd.DataFrame) -> list[dict]: rows: list[dict] = [] for _, row in df.iterrows(): payload: dict = {} for key, value in row.items(): if hasattr(value, "isoformat"): payload[str(key)] = value.isoformat() else: payload[str(key)] = value rows.append(payload) return rows @asynccontextmanager async def lifespan(app: FastAPI): telemetry: TelemetryProviders = configure_otel(settings) engines = create_warehouse_engines() instrument_sqlalchemy_engines(engines) app.state.query_client = ReadOnlyWarehouseClient(engines) LOGGER.info("BI query service ready with read-only MSSQL engines") yield dispose_engines(engines) shutdown_otel(telemetry) app = FastAPI(title="bi-query-service", version="0.1.0", lifespan=lifespan) instrument_fastapi(app) @app.get("/internal/health") def health(response: Response) -> dict: response.headers.update(current_trace_headers()) return {"status": "ok", "service": "bi-query-service"} @app.get("/internal/daily-sales") def daily_sales( response: Response, _auth: InternalPrincipal = Depends(require_internal_principal) ) -> list[dict]: response.headers.update(current_trace_headers()) client: ReadOnlyWarehouseClient = app.state.query_client return _frame_to_rows(client.fetch_daily_sales()) @app.get("/internal/product-performance") def product_performance( response: Response, _auth: InternalPrincipal = Depends(require_internal_principal) ) -> list[dict]: response.headers.update(current_trace_headers()) client: ReadOnlyWarehouseClient = app.state.query_client return _frame_to_rows(client.fetch_product_performance()) @app.get("/internal/customer-performance") def customer_performance( response: Response, _auth: InternalPrincipal = Depends(require_internal_principal) ) -> list[dict]: response.headers.update(current_trace_headers()) client: ReadOnlyWarehouseClient = app.state.query_client return _frame_to_rows(client.fetch_customer_performance())