first
This commit is contained in:
@@ -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.",
|
||||
}
|
||||
Reference in New Issue
Block a user