Coverage for app/main.py: 68.85%
61 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-25 15:51 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-25 15:51 +0000
1import asyncio
2import logging
3import os
4import threading
5from contextlib import asynccontextmanager
7from fastapi import FastAPI
8from fastapi.middleware.cors import CORSMiddleware
10from app.config import settings
11from app.routers import (
12 admin_apps,
13 apps,
14 auth_keycloak,
15 courses,
16 dashboard,
17 deployments,
18 openstack_credentials,
19 openstack_resources,
20 quotas,
21 tasks,
22 teams,
23 users,
24)
25from app.services.celery_event_listener import start_event_listener
26from app.services.deployment_pubsub import pubsub
27from app.services.reconciler import run_reconciler
29logger = logging.getLogger(__name__)
32# ``DISABLE_BACKGROUND_TASKS`` short-circuits the lifespan body so the
33# app is fully wired but the Celery listener and reconciler are not
34# started. Used by the test suite, where per-TestClient lifespans would
35# otherwise stack daemon threads and exhaust the DB connection pool.
36def _background_tasks_disabled() -> bool:
37 return os.getenv("DISABLE_BACKGROUND_TASKS", "").lower() in ("1", "true", "yes")
40# ----------------------------------------------------------------
41# STARTUP/SHUTDOWN
42# ----------------------------------------------------------------
43@asynccontextmanager
44async def lifespan(app: FastAPI):
45 # Startup
46 logger.info("=== Application Starting ===")
47 logger.info("ℹ️ Use 'alembic upgrade head' to apply database migrations")
49 if _background_tasks_disabled():
50 # Test path: keep ``app`` fully functional but skip the Celery
51 # listener + reconciler.
52 logger.info(
53 "DISABLE_BACKGROUND_TASKS set — skipping Celery listener "
54 "and reconciler (test mode)"
55 )
56 try:
57 yield
58 finally:
59 logger.info("=== Application Shutting Down (test mode) ===")
60 return
62 # Bind the FastAPI event loop to the deployment pubsub *before*
63 # spawning the Celery listener. The listener thread pushes into
64 # the pubsub from a non-asyncio thread; without a loop reference
65 # those pushes would be silently dropped.
66 pubsub.set_loop(asyncio.get_running_loop())
67 logger.info("Deployment pubsub bound to event loop")
69 # Start Celery event listener in background thread
70 listener_thread = threading.Thread(target=start_event_listener, daemon=True)
71 listener_thread.start()
72 logger.info("Celery event listener started in background")
74 # Reconciler is the safety net for events the listener missed (lost
75 # event, backend restart during dispatch, broker hiccups). It runs
76 # as an asyncio task so we can cancel it cleanly on shutdown.
77 reconciler_task = asyncio.create_task(run_reconciler())
78 logger.info("Reconciler loop scheduled")
80 logger.info("Application started")
82 try:
83 yield
84 finally:
85 # Shutdown
86 logger.info("=== Application Shutting Down ===")
87 reconciler_task.cancel()
88 try:
89 await reconciler_task
90 except asyncio.CancelledError:
91 pass
92 except Exception:
93 logger.exception("Reconciler task raised on shutdown")
94 logger.info("Shutdown complete")
97# ----------------------------------------------------------------
98# FASTAPI APP
99# ----------------------------------------------------------------
100app = FastAPI(
101 title="Backend API",
102 description="FastAPI Backend with Auth, Git & Celery Integration",
103 version="1.0.0",
104 lifespan=lifespan
105)
107# ----------------------------------------------------------------
108# CORS
109# ----------------------------------------------------------------
110app.add_middleware(
111 CORSMiddleware,
112 allow_origins=settings.CORS_ORIGINS,
113 allow_credentials=True,
114 allow_methods=["*"],
115 allow_headers=["*"],
116)
118# ----------------------------------------------------------------
119# ROUTERS
120# ----------------------------------------------------------------
121app.include_router(auth_keycloak.router, prefix="/auth", tags=["Authentication"])
122app.include_router(users.router, prefix="/users", tags=["Users"])
123app.include_router(courses.router, prefix="/courses", tags=["Courses"])
124app.include_router(apps.router, prefix="/apps", tags=["Apps"])
125app.include_router(admin_apps.router, prefix="/admin", tags=["Admin"])
126app.include_router(deployments.router, prefix="/deployments", tags=["Deployments"])
127app.include_router(tasks.router, prefix="/tasks", tags=["Tasks"])
128app.include_router(teams.router, prefix="/teams", tags=["Teams"])
129app.include_router(quotas.router, prefix="/quotas", tags=["Quotas"])
130app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"])
131app.include_router(openstack_credentials.router, tags=["OpenStack Credentials"])
132# Read API for OpenStack resources (Networks, Flavors, Images, ...),
133# used by the wizard's value-help dropdowns so users don't have to type
134# UUIDs from Horizon.
135app.include_router(
136 openstack_resources.router,
137 prefix="/me/openstack/resources",
138 tags=["OpenStack Resources"],
139)
142# ----------------------------------------------------------------
143# HEALTH CHECK
144# ----------------------------------------------------------------
145@app.get("/health")
146def health_check():
147 return {
148 "status": "healthy",
149 "service": "backend-api",
150 "version": "1.0.0"
151 }