This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
"""ASF Core — FastAPI application entrypoint.
Run locally with:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
"""
from __future__ import annotations
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from .config import get_settings
from .logging_config import setup_logging
from .middleware.correlation import CorrelationMiddleware
from .db.mongo import mongo_lifespan, create_indexes, close_mongo
from .messaging.rabbitmq import (
rabbitmq_lifespan,
close_rabbitmq,
get_event_publisher,
)
from .api.v1 import api_v1_router
from .repositories import (
NotFoundError,
DuplicateError,
TenantMismatchError,
)
setup_logging()
s = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI): # noqa: ARG001 - signature required by FastAPI
"""Startup / shutdown: ensure indexes, open/close connections."""
import structlog
log = structlog.get_logger(__name__)
try:
await create_indexes()
except Exception: # noqa: BLE001
await log.aexception("mongo_indexes_failed")
try:
await get_event_publisher()
except Exception: # noqa: BLE001
# If RabbitMQ isn't reachable yet we still start the API — core
# writes Mongo first and events are best-effort during startup.
await log.awarning("rabbitmq_not_available_on_startup")
yield
await close_rabbitmq()
await close_mongo()
await log.ainfo("asf_core_shutdown")
# ---------------------------------------------------------------------------
# App object
# ---------------------------------------------------------------------------
app = FastAPI(
title="Azurian Software Factory (ASF) — Core API",
description=(
"Multi-tenant REST API for Azurian Software Factory. "
"Handles Organizations, Projects, Artifacts, Artifact Relationships, "
"Jobs, Audit events, and dashboard aggregations. "
"All business operations must go through this API — "
"never access MongoDB / RabbitMQ directly from the browser."
),
version="0.1.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
lifespan=lifespan,
)
# ---------------------------------------------------------------------------
# Middleware
# ---------------------------------------------------------------------------
app.add_middleware(
CORSMiddleware,
allow_origins=s.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=[
"X-Correlation-Id",
],
)
# Correlation / context (tenant + user id extraction in dev-trust mode)
app.add_middleware(CorrelationMiddleware)
# ---------------------------------------------------------------------------
# Error handlers — translate repository exceptions to consistent HTTP shapes
# ---------------------------------------------------------------------------
@app.exception_handler(NotFoundError)
async def _not_found(req: Request, exc: NotFoundError) -> JSONResponse: # noqa: ARG001
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"error": "not_found",
"kind": exc.kind,
"entity_id": str(exc.entity_id),
"message": str(exc),
},
)
@app.exception_handler(DuplicateError)
async def _duplicate(req: Request, exc: DuplicateError) -> JSONResponse: # noqa: ARG001
return JSONResponse(
status_code=status.HTTP_409_CONFLICT,
content={
"error": "duplicate",
"kind": exc.kind,
"key": exc.key,
"message": str(exc),
},
)
@app.exception_handler(TenantMismatchError)
async def _tenant_mismatch(req: Request, exc: TenantMismatchError) -> JSONResponse: # noqa: ARG001
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"error": "tenant_isolation_violation",
"kind": exc.kind,
"entity_id": str(exc.entity_id),
"message": str(exc),
},
)
@app.exception_handler(Exception)
async def _unhandled(req: Request, exc: Exception) -> JSONResponse: # noqa: ARG001
import structlog
await structlog.get_logger(__name__).aexception("unhandled_exception")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "internal_server_error",
"message": "An unexpected error occurred. The correlation id has been logged.",
},
)
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
app.include_router(api_v1_router, prefix=s.api_prefix)
@app.get("/", tags=["meta"])
async def root() -> dict:
return {
"name": "asf-core",
"version": "0.1.0",
"docs": "/docs",
"health": f"{s.api_prefix}/health",
"tagline": "From Requirement to Production, everything is contextualized, traceable and AI-accessible.",
}