- Adapter اکانتمحور با چرخش round-robin و بنچشدن خودکار روی ۴۲۹/۴۰۱ - ورود ادمین از طریق URL بازگشتی گوگل (PKCE) و ذخیرهٔ رمزنگاریشدهٔ refresh token - صفحهٔ مدیریت اکانتهای agy در پنل ادمین به همراه تست و فعال/غیرفعالسازی - مهاجرت Alembic b1c2d3e4f5a6 برای ستونهای اکانت agy - seed همیشه کاتالوگ agy را میسازد و فقط با AI_DEFAULT_PROVIDER=agy مسیریابی میشود
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""FastAPI application factory.
|
|
|
|
Nothing in the inner layers imports FastAPI; this module wires them together.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import Base, engine
|
|
from app.interfaces.http.errors import register_exception_handlers
|
|
from app.interfaces.http.request_context import RequestContextMiddleware
|
|
from app.interfaces.http.routers import (
|
|
admin,
|
|
agy,
|
|
auth,
|
|
billing,
|
|
health,
|
|
knowledge,
|
|
organizations,
|
|
projects,
|
|
sandbox,
|
|
workforce,
|
|
)
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger("agy.api")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
try:
|
|
# In development we create any missing tables so `pnpm dev` works on a
|
|
# fresh clone. Production schema changes go through Alembic
|
|
# (`pnpm db:migrate`).
|
|
if not settings.is_production:
|
|
import app.infrastructure.db.models # noqa: F401 - register metadata
|
|
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
logger.info("Development schema ensured.")
|
|
logger.info(
|
|
"Adapters -> ai=%s git=%s domain=%s sandbox=%s payment=%s",
|
|
settings.ai_default_provider,
|
|
settings.git_provider,
|
|
settings.domain_router,
|
|
settings.sandbox_provider,
|
|
settings.payment_provider,
|
|
)
|
|
except Exception as exc:
|
|
# Startup failures are the hardest to diagnose after the fact: the
|
|
# process is gone and all that remains is whatever reached stderr, which
|
|
# a container runtime routinely discards. Record it, then still fail
|
|
# loudly — a half-started API must not accept traffic.
|
|
await _record_startup_failure(exc)
|
|
raise
|
|
yield
|
|
|
|
|
|
async def _record_startup_failure(exc: BaseException) -> None:
|
|
"""Persist a boot failure. Never raises; the caller re-raises regardless."""
|
|
try:
|
|
from app.domain.audit.models import ErrorSeverity, ErrorSource
|
|
from app.infrastructure.error_log import record_error
|
|
|
|
await record_error(
|
|
kind="startup_failed",
|
|
message=str(exc) or type(exc).__name__,
|
|
exc=exc,
|
|
source=ErrorSource.STARTUP,
|
|
severity=ErrorSeverity.CRITICAL,
|
|
metadata={"app_env": settings.app_env},
|
|
)
|
|
except Exception: # pragma: no cover - boot is already failing
|
|
logger.exception("Could not record the startup failure.")
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
application = FastAPI(
|
|
title=settings.app_name,
|
|
version="0.1.0",
|
|
description="AI Workforce Platform — standalone multi-tenant SaaS.",
|
|
lifespan=lifespan,
|
|
docs_url="/docs",
|
|
openapi_url="/openapi.json",
|
|
)
|
|
|
|
# Starlette applies middleware outermost-first in reverse registration
|
|
# order, so the last one added wraps everything else. The request-context
|
|
# middleware goes last on purpose: that way even a CORS rejection gets an
|
|
# ``X-Request-ID`` — and a CORS rejection is exactly the kind of opaque
|
|
# failure where a correlatable id is worth having.
|
|
application.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origin_list,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
# Without this the browser cannot read the header off a cross-origin
|
|
# response, and the id would be useless to the web client.
|
|
expose_headers=["X-Request-ID"],
|
|
)
|
|
|
|
application.add_middleware(RequestContextMiddleware)
|
|
|
|
register_exception_handlers(application)
|
|
|
|
application.include_router(health.router)
|
|
application.include_router(auth.router, prefix="/api")
|
|
application.include_router(organizations.router, prefix="/api")
|
|
application.include_router(projects.router, prefix="/api")
|
|
application.include_router(workforce.router, prefix="/api")
|
|
application.include_router(billing.router, prefix="/api")
|
|
application.include_router(knowledge.router, prefix="/api")
|
|
application.include_router(sandbox.router, prefix="/api")
|
|
application.include_router(admin.router, prefix="/api")
|
|
application.include_router(agy.router, prefix="/api")
|
|
|
|
@application.get("/", tags=["meta"])
|
|
async def root() -> dict:
|
|
return {
|
|
"name": settings.app_name,
|
|
"version": "0.1.0",
|
|
"docs": "/docs",
|
|
"health": "/health",
|
|
}
|
|
|
|
return application
|
|
|
|
|
|
app = create_app()
|