semantics
This commit is contained in:
@@ -32,19 +32,43 @@ from .core.logger import logger, belief_scope
|
||||
from .core.database import AuthSessionLocal
|
||||
from .core.auth.security import get_password_hash
|
||||
from .models.auth import User, Role
|
||||
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, clean_release_v2, profile, health, dataset_review
|
||||
from .api.routes import (
|
||||
plugins,
|
||||
tasks,
|
||||
settings,
|
||||
environments,
|
||||
mappings,
|
||||
migration,
|
||||
connections,
|
||||
git,
|
||||
storage,
|
||||
admin,
|
||||
llm,
|
||||
dashboards,
|
||||
datasets,
|
||||
reports,
|
||||
assistant,
|
||||
clean_release,
|
||||
clean_release_v2,
|
||||
profile,
|
||||
health,
|
||||
dataset_review,
|
||||
)
|
||||
from .api import auth
|
||||
|
||||
# [DEF:App:Global]
|
||||
# @COMPLEXITY: 1
|
||||
# @SEMANTICS: app, fastapi, instance
|
||||
# @PURPOSE: The global FastAPI application instance.
|
||||
# [DEF:FastAPI_App:Global]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: app, fastapi, instance, route-registry
|
||||
# @PURPOSE: Canonical FastAPI application instance for route, middleware, and websocket registration.
|
||||
# @RELATION: DEPENDS_ON -> [ApiRoutesModule]
|
||||
# @RELATION: BINDS_TO -> [API_Routes]
|
||||
app = FastAPI(
|
||||
title="Superset Tools API",
|
||||
description="API for managing Superset automation tools and plugins.",
|
||||
version="1.0.0",
|
||||
)
|
||||
# [/DEF:App:Global]
|
||||
# [/DEF:FastAPI_App:Global]
|
||||
|
||||
|
||||
# [DEF:ensure_initial_admin_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
@@ -72,7 +96,9 @@ def ensure_initial_admin_user() -> None:
|
||||
|
||||
existing_user = db.query(User).filter(User.username == username).first()
|
||||
if existing_user:
|
||||
logger.info("Initial admin bootstrap skipped: user '%s' already exists.", username)
|
||||
logger.info(
|
||||
"Initial admin bootstrap skipped: user '%s' already exists.", username
|
||||
)
|
||||
return
|
||||
|
||||
new_user = User(
|
||||
@@ -85,15 +111,20 @@ def ensure_initial_admin_user() -> None:
|
||||
new_user.roles.append(admin_role)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
logger.info("Initial admin user '%s' created from environment bootstrap.", username)
|
||||
logger.info(
|
||||
"Initial admin user '%s' created from environment bootstrap.", username
|
||||
)
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error("Failed to bootstrap initial admin user: %s", exc)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# [/DEF:ensure_initial_admin_user:Function]
|
||||
|
||||
|
||||
# [DEF:startup_event:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Handles application startup tasks, such as starting the scheduler.
|
||||
@@ -108,8 +139,11 @@ async def startup_event():
|
||||
ensure_initial_admin_user()
|
||||
scheduler = get_scheduler_service()
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# [/DEF:startup_event:Function]
|
||||
|
||||
|
||||
# [DEF:shutdown_event:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Handles application shutdown tasks, such as stopping the scheduler.
|
||||
@@ -122,12 +156,15 @@ async def shutdown_event():
|
||||
with belief_scope("shutdown_event"):
|
||||
scheduler = get_scheduler_service()
|
||||
scheduler.stop()
|
||||
|
||||
|
||||
# [/DEF:shutdown_event:Function]
|
||||
|
||||
# [DEF:app_middleware:Block]
|
||||
# @PURPOSE: Configure application-wide middleware (Session, CORS).
|
||||
# Configure Session Middleware (required by Authlib for OAuth2 flow)
|
||||
from .core.auth.config import auth_config
|
||||
|
||||
app.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)
|
||||
|
||||
# Configure CORS
|
||||
@@ -154,10 +191,13 @@ async def network_error_handler(request: Request, exc: NetworkError):
|
||||
logger.error(f"Network error: {exc}")
|
||||
return HTTPException(
|
||||
status_code=503,
|
||||
detail="Environment unavailable. Please check if the Superset instance is running."
|
||||
detail="Environment unavailable. Please check if the Superset instance is running.",
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:network_error_handler:Function]
|
||||
|
||||
|
||||
# [DEF:log_requests:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Middleware to log incoming HTTP requests and their response status.
|
||||
@@ -171,32 +211,50 @@ async def log_requests(request: Request, call_next):
|
||||
with belief_scope("log_requests"):
|
||||
# Avoid spamming logs for polling endpoints
|
||||
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET"
|
||||
|
||||
|
||||
if not is_polling:
|
||||
logger.info(f"Incoming request: {request.method} {request.url.path}")
|
||||
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
if not is_polling:
|
||||
logger.info(f"Response status: {response.status_code} for {request.url.path}")
|
||||
logger.info(
|
||||
f"Response status: {response.status_code} for {request.url.path}"
|
||||
)
|
||||
return response
|
||||
except NetworkError as e:
|
||||
logger.error(f"Network error caught in middleware: {e}")
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Environment unavailable. Please check if the Superset instance is running."
|
||||
detail="Environment unavailable. Please check if the Superset instance is running.",
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:log_requests:Function]
|
||||
|
||||
# [DEF:api_routes:Block]
|
||||
# @PURPOSE: Register all application API routers.
|
||||
# [DEF:API_Routes:Block]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Register all FastAPI route groups exposed by the application entrypoint.
|
||||
# @RELATION: DEPENDS_ON -> [FastAPI_App]
|
||||
# @RELATION: DEPENDS_ON -> [Route_Group_Contracts]
|
||||
# @RELATION: DEPENDS_ON -> [AuthApi]
|
||||
# @RELATION: DEPENDS_ON -> [AdminApi]
|
||||
# @RELATION: DEPENDS_ON -> [PluginsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [TasksRouter]
|
||||
# @RELATION: DEPENDS_ON -> [SettingsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [ConnectionsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [ReportsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [LlmRoutes]
|
||||
# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api]
|
||||
# Include API routes
|
||||
app.include_router(auth.router)
|
||||
app.include_router(admin.router)
|
||||
app.include_router(plugins.router, prefix="/api/plugins", tags=["Plugins"])
|
||||
app.include_router(tasks.router, prefix="/api/tasks", tags=["Tasks"])
|
||||
app.include_router(settings.router, prefix="/api/settings", tags=["Settings"])
|
||||
app.include_router(connections.router, prefix="/api/settings/connections", tags=["Connections"])
|
||||
app.include_router(
|
||||
connections.router, prefix="/api/settings/connections", tags=["Connections"]
|
||||
)
|
||||
app.include_router(environments.router, tags=["Environments"])
|
||||
app.include_router(mappings.router, prefix="/api/mappings", tags=["Mappings"])
|
||||
app.include_router(migration.router)
|
||||
@@ -212,7 +270,7 @@ app.include_router(clean_release_v2.router)
|
||||
app.include_router(profile.router)
|
||||
app.include_router(dataset_review.router)
|
||||
app.include_router(health.router)
|
||||
# [/DEF:api_routes:Block]
|
||||
# [/DEF:API_Routes:Block]
|
||||
|
||||
|
||||
# [DEF:api.include_routers:Action]
|
||||
@@ -222,6 +280,7 @@ app.include_router(health.router)
|
||||
# @SEMANTICS: routes, registration, api
|
||||
# [/DEF:api.include_routers:Action]
|
||||
|
||||
|
||||
# [DEF:websocket_endpoint:Function]
|
||||
# @COMPLEXITY: 5
|
||||
# @PURPOSE: Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.
|
||||
@@ -250,14 +309,11 @@ app.include_router(health.router)
|
||||
# @TEST_INVARIANT: consistent_streaming -> verifies: [valid_ws_connection]
|
||||
@app.websocket("/ws/logs/{task_id}")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
task_id: str,
|
||||
source: str = None,
|
||||
level: str = None
|
||||
websocket: WebSocket, task_id: str, source: str = None, level: str = None
|
||||
):
|
||||
"""
|
||||
WebSocket endpoint for real-time log streaming with optional server-side filtering.
|
||||
|
||||
|
||||
Query Parameters:
|
||||
source: Filter logs by source component (e.g., "plugin", "superset_api")
|
||||
level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)
|
||||
@@ -327,7 +383,9 @@ async def websocket_endpoint(
|
||||
task = task_manager.get_task(task_id)
|
||||
if task and task.status == "AWAITING_INPUT" and task.input_request:
|
||||
synthetic_log = {
|
||||
"timestamp": task.logs[-1].timestamp.isoformat() if task.logs else "2024-01-01T00:00:00",
|
||||
"timestamp": task.logs[-1].timestamp.isoformat()
|
||||
if task.logs
|
||||
else "2024-01-01T00:00:00",
|
||||
"level": "INFO",
|
||||
"message": "Task paused for user input (Connection Re-established)",
|
||||
"context": {"input_request": task.input_request},
|
||||
@@ -355,7 +413,10 @@ async def websocket_endpoint(
|
||||
},
|
||||
)
|
||||
|
||||
if "Task completed successfully" in log_entry.message or "Task failed" in log_entry.message:
|
||||
if (
|
||||
"Task completed successfully" in log_entry.message
|
||||
or "Task failed" in log_entry.message
|
||||
):
|
||||
logger.reason(
|
||||
"Observed terminal task log entry; delaying to preserve client visibility",
|
||||
extra={"task_id": task_id, "message": log_entry.message},
|
||||
@@ -379,6 +440,8 @@ async def websocket_endpoint(
|
||||
"Released WebSocket log queue subscription",
|
||||
extra={"task_id": task_id},
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:websocket_endpoint:Function]
|
||||
|
||||
# [DEF:StaticFiles:Mount]
|
||||
@@ -387,7 +450,9 @@ async def websocket_endpoint(
|
||||
# @PURPOSE: Mounts the frontend build directory to serve static assets.
|
||||
frontend_path = project_root / "frontend" / "build"
|
||||
if frontend_path.exists():
|
||||
app.mount("/_app", StaticFiles(directory=str(frontend_path / "_app")), name="static")
|
||||
app.mount(
|
||||
"/_app", StaticFiles(directory=str(frontend_path / "_app")), name="static"
|
||||
)
|
||||
|
||||
# [DEF:serve_spa:Function]
|
||||
# @COMPLEXITY: 1
|
||||
@@ -399,15 +464,22 @@ if frontend_path.exists():
|
||||
with belief_scope("serve_spa"):
|
||||
# Only serve SPA for non-API paths
|
||||
# API routes are registered separately and should be matched by FastAPI first
|
||||
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
|
||||
if file_path and (
|
||||
file_path.startswith("api/")
|
||||
or file_path.startswith("/api/")
|
||||
or file_path == "api"
|
||||
):
|
||||
# This should not happen if API routers are properly registered
|
||||
# Return 404 instead of serving HTML
|
||||
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"API endpoint not found: {file_path}"
|
||||
)
|
||||
|
||||
full_path = frontend_path / file_path
|
||||
if file_path and full_path.is_file():
|
||||
return FileResponse(str(full_path))
|
||||
return FileResponse(str(frontend_path / "index.html"))
|
||||
|
||||
# [/DEF:serve_spa:Function]
|
||||
else:
|
||||
# [DEF:read_root:Function]
|
||||
@@ -418,7 +490,10 @@ else:
|
||||
@app.get("/")
|
||||
async def read_root():
|
||||
with belief_scope("read_root"):
|
||||
return {"message": "Superset Tools API is running (Frontend build not found)"}
|
||||
return {
|
||||
"message": "Superset Tools API is running (Frontend build not found)"
|
||||
}
|
||||
|
||||
# [/DEF:read_root:Function]
|
||||
# [/DEF:StaticFiles:Mount]
|
||||
# [/DEF:AppModule:Module]
|
||||
|
||||
Reference in New Issue
Block a user