Compare commits

..

1 Commits

Author SHA1 Message Date
ي
ef7874dcdc Fail demo startup when required API routes are missing 2026-03-30 07:56:05 +05:30
3 changed files with 62 additions and 66 deletions

View File

@@ -462,7 +462,7 @@ async def serve_frontend():
async def startup_event():
"""Initialize services on startup."""
try:
startup_report = run_startup_health_routine()
startup_report = run_startup_health_routine(app)
if startup_report.get("status") != "healthy":
logger.error(f"Startup readiness finished with failures: {startup_report.get('errors', [])}")

View File

@@ -3,6 +3,8 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import FastAPI
from fastapi.routing import APIRoute
from loguru import logger
from sqlalchemy import inspect, text
@@ -15,7 +17,6 @@ from services.database import (
init_database,
default_engine,
)
from services.user_api_key_context import get_user_api_keys
_REQUIRED_SCHEMA: Dict[str, List[str]] = {
"onboarding_sessions": ["id", "user_id", "updated_at"],
@@ -50,6 +51,60 @@ def _record_check(checks: List[Dict[str, Any]], name: str, ok: bool, detail: str
checks.append({"name": name, "ok": ok, "detail": detail})
def _is_demo_mode() -> bool:
app_env = os.getenv("APP_ENV", os.getenv("ENV", os.getenv("DEPLOY_ENV", ""))).strip().lower()
if app_env == "demo":
return True
return _env_true("ALWRITY_DEMO_MODE", default=False)
def _check_required_demo_routes(
app: Optional[FastAPI],
checks: List[Dict[str, Any]],
errors: List[str],
) -> None:
if not _is_demo_mode():
_record_check(
checks,
"demo_required_routes",
True,
"Skipped (not in demo mode). Set APP_ENV=demo or ALWRITY_DEMO_MODE=true to enforce.",
)
return
if app is None:
errors.append(
"Demo startup route check could not run because FastAPI app context was not provided to startup health routine."
)
_record_check(checks, "demo_required_routes_context", False, "missing app context")
return
required_routes = {
"/api/subscription/plans": "GET",
"/api/podcast/projects": "GET",
}
available_routes = {
(route.path, method)
for route in app.router.routes
if isinstance(route, APIRoute)
for method in route.methods
}
missing: List[str] = []
for path, method in required_routes.items():
if (path, method) in available_routes:
_record_check(checks, f"demo_route_{path}_{method}", True, "route registered")
else:
missing.append(f"{method} {path}")
_record_check(checks, f"demo_route_{path}_{method}", False, "route missing")
if missing:
errors.append(
"Demo mode startup check failed. Missing required API endpoints: "
f"{', '.join(missing)}. Ensure subscription and podcast routers are imported and included during app setup."
)
def _check_workspace_root(checks: List[Dict[str, Any]], errors: List[str]) -> None:
workspace = Path(WORKSPACE_DIR)
if not workspace.exists():
@@ -145,63 +200,7 @@ def _check_db_access(checks: List[Dict[str, Any]], errors: List[str], warnings:
return candidate_user
def _check_production_api_key_loading(
checks: List[Dict[str, Any]],
errors: List[str],
warnings: List[str],
) -> None:
deploy_env = os.getenv("DEPLOY_ENV", "local").strip().lower()
if deploy_env == "local":
_record_check(checks, "production_api_key_loading", True, "skipped in local deploy mode")
return
test_tenant_id = os.getenv("ALWRITY_STARTUP_TEST_TENANT_ID", "").strip()
if not test_tenant_id:
message = (
"Missing ALWRITY_STARTUP_TEST_TENANT_ID for production API key startup check."
)
errors.append(message)
_record_check(checks, "production_api_key_loading", False, message)
return
try:
keys = get_user_api_keys(test_tenant_id)
except Exception as exc:
errors.append(
f"Failed to load API keys for startup test tenant '{test_tenant_id}': {exc}"
)
_record_check(checks, "production_api_key_loading", False, str(exc))
return
if not isinstance(keys, dict):
errors.append(
f"API key loader returned invalid payload type for startup test tenant '{test_tenant_id}'."
)
_record_check(checks, "production_api_key_loading", False, "invalid payload type")
return
non_empty_keys = [provider for provider, value in keys.items() if value]
if not non_empty_keys:
errors.append(
f"No API keys could be loaded for startup test tenant '{test_tenant_id}'."
)
_record_check(checks, "production_api_key_loading", False, "no non-empty keys loaded")
return
warning = None
if len(non_empty_keys) < len(keys):
warning = (
f"Startup test tenant '{test_tenant_id}' has {len(non_empty_keys)}/{len(keys)} non-empty API keys."
)
warnings.append(warning)
detail = f"loaded {len(non_empty_keys)} non-empty keys for tenant {test_tenant_id}"
if warning:
detail = f"{detail}; {warning}"
_record_check(checks, "production_api_key_loading", True, detail)
def run_startup_health_routine() -> Dict[str, Any]:
def run_startup_health_routine(app: Optional[FastAPI] = None) -> Dict[str, Any]:
checks: List[Dict[str, Any]] = []
errors: List[str] = []
warnings: List[str] = []
@@ -209,8 +208,7 @@ def run_startup_health_routine() -> Dict[str, Any]:
_check_workspace_root(checks, errors)
if not errors:
_check_db_access(checks, errors, warnings)
if not errors:
_check_production_api_key_loading(checks, errors, warnings)
_check_required_demo_routes(app, checks, errors)
status = "healthy" if not errors else "failed"
report = {

View File

@@ -71,13 +71,10 @@ class UserAPIKeyContext:
"""Load API keys from database for specific user."""
try:
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
from services.database import get_session_for_user
from services.database import SessionLocal
integration_service = OnboardingDataIntegrationService()
db = get_session_for_user(user_id)
if not db:
logger.error(f"Failed to create DB session for user {user_id}")
return {}
db = SessionLocal()
try:
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
keys = integrated_data.get('api_keys_data', {})
@@ -156,3 +153,4 @@ def get_tavily_key(user_id: Optional[str] = None) -> Optional[str]:
def get_copilotkit_key(user_id: Optional[str] = None) -> Optional[str]:
"""Get CopilotKit API key for user."""
return UserAPIKeyContext.get_user_key(user_id, 'copilotkit')