Initial commit: CI Agent competitive-intelligence monitoring app

FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
This commit is contained in:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
"""FastAPI application entrypoint."""
from __future__ import annotations
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, Response
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from structlog.contextvars import bound_contextvars
from app.api.v1.router import api_v1_router
from app.core.config import get_settings
from app.core.errors import (
AppError,
AuthenticationError,
ConflictError,
ForbiddenError,
NotFoundError,
RateLimitedError,
ThrottledError,
ValidationAppError,
)
from app.core.logging import configure_logging, get_logger
from app.core.rate_limit import limiter
settings = get_settings()
configure_logging(settings)
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(_app: FastAPI):
logger.info(
"startup",
app_env=settings.app_env,
auth_mode=settings.auth_mode,
llm_provider=settings.llm_provider,
)
yield
logger.info("shutdown")
app = FastAPI(
title=settings.app_name,
version="0.1.0",
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(
CORSMiddleware,
# FRONTEND_URL may be a comma-separated list (e.g. localhost plus a LAN
# address) so the same API can serve a browser on this machine and one
# elsewhere on the network at once.
allow_origins=[origin.strip() for origin in settings.frontend_url.split(",") if origin.strip()],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def correlation_id_middleware(request: Request, call_next) -> Response:
"""Every log line emitted while handling this request carries the same
request_id (structlog contextvars, merged in automatically - see
core/logging.py), and the id is echoed back so a client/proxy log can be
cross-referenced with ours. Reuses an inbound X-Request-ID if a gateway
already set one, rather than always minting a fresh id."""
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
with bound_contextvars(request_id=request_id):
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
_APP_ERROR_STATUS_CODES: dict[type[AppError], int] = {
NotFoundError: status.HTTP_404_NOT_FOUND,
ConflictError: status.HTTP_409_CONFLICT,
AuthenticationError: status.HTTP_401_UNAUTHORIZED,
ForbiddenError: status.HTTP_403_FORBIDDEN,
ValidationAppError: status.HTTP_400_BAD_REQUEST,
RateLimitedError: status.HTTP_429_TOO_MANY_REQUESTS,
ThrottledError: status.HTTP_429_TOO_MANY_REQUESTS,
}
@app.exception_handler(AppError)
async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse:
status_code = _APP_ERROR_STATUS_CODES.get(type(exc), status.HTTP_400_BAD_REQUEST)
content: dict = {"detail": str(exc)}
headers: dict[str, str] = {}
if isinstance(exc, ThrottledError) and exc.retry_after_seconds is not None:
content["retry_after_seconds"] = exc.retry_after_seconds
headers["Retry-After"] = str(exc.retry_after_seconds)
return JSONResponse(status_code=status_code, content=content, headers=headers)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
_request: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
content={"detail": "Invalid request", "errors": jsonable_encoder(exc.errors())},
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(_request: Request, exc: Exception) -> JSONResponse:
logger.error("unhandled_exception", error=str(exc), error_type=type(exc).__name__)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Internal server error"},
)
@app.get("/health", include_in_schema=False)
async def root_health() -> dict[str, str]:
"""Plain root-level liveness probe for infra tooling (Docker, etc.)."""
return {"status": "ok"}
app.include_router(api_v1_router)