# [DEF:SettingsRouter:Module] # # @SEMANTICS: settings, api, router, fastapi # @PURPOSE: Provides API endpoints for managing application settings and Superset environments. # @LAYER: UI (API) # @RELATION: DEPENDS_ON -> ConfigManager # @RELATION: DEPENDS_ON -> ConfigModels # # @INVARIANT: All settings changes must be persisted via ConfigManager. # @PUBLIC_API: router # [SECTION: IMPORTS] from fastapi import APIRouter, Depends, HTTPException from typing import List from pydantic import BaseModel from ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig from ...models.storage import StorageConfig from ...dependencies import get_config_manager, has_permission from ...core.config_manager import ConfigManager from ...core.logger import logger, belief_scope from ...core.superset_client import SupersetClient # [/SECTION] # [DEF:LoggingConfigResponse:Class] # @PURPOSE: Response model for logging configuration with current task log level. # @SEMANTICS: logging, config, response class LoggingConfigResponse(BaseModel): level: str task_log_level: str enable_belief_state: bool # [/DEF:LoggingConfigResponse:Class] router = APIRouter() # [DEF:get_settings:Function] # @PURPOSE: Retrieves all application settings. # @PRE: Config manager is available. # @POST: Returns masked AppConfig. # @RETURN: AppConfig - The current configuration. @router.get("", response_model=AppConfig) async def get_settings( config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_settings"): logger.info("[get_settings][Entry] Fetching all settings") config = config_manager.get_config().copy(deep=True) # Mask passwords for env in config.environments: if env.password: env.password = "********" return config # [/DEF:get_settings:Function] # [DEF:update_global_settings:Function] # @PURPOSE: Updates global application settings. # @PRE: New settings are provided. # @POST: Global settings are updated. # @PARAM: settings (GlobalSettings) - The new global settings. # @RETURN: GlobalSettings - The updated settings. @router.patch("/global", response_model=GlobalSettings) async def update_global_settings( settings: GlobalSettings, config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("update_global_settings"): logger.info("[update_global_settings][Entry] Updating global settings") config_manager.update_global_settings(settings) return settings # [/DEF:update_global_settings:Function] # [DEF:get_storage_settings:Function] # @PURPOSE: Retrieves storage-specific settings. # @RETURN: StorageConfig - The storage configuration. @router.get("/storage", response_model=StorageConfig) async def get_storage_settings( config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_storage_settings"): return config_manager.get_config().settings.storage # [/DEF:get_storage_settings:Function] # [DEF:update_storage_settings:Function] # @PURPOSE: Updates storage-specific settings. # @PARAM: storage (StorageConfig) - The new storage settings. # @POST: Storage settings are updated and saved. # @RETURN: StorageConfig - The updated storage settings. @router.put("/storage", response_model=StorageConfig) async def update_storage_settings( storage: StorageConfig, config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("update_storage_settings"): is_valid, message = config_manager.validate_path(storage.root_path) if not is_valid: raise HTTPException(status_code=400, detail=message) settings = config_manager.get_config().settings settings.storage = storage config_manager.update_global_settings(settings) return config_manager.get_config().settings.storage # [/DEF:update_storage_settings:Function] # [DEF:get_environments:Function] # @PURPOSE: Lists all configured Superset environments. # @PRE: Config manager is available. # @POST: Returns list of environments. # @RETURN: List[Environment] - List of environments. @router.get("/environments", response_model=List[Environment]) async def get_environments( config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_environments"): logger.info("[get_environments][Entry] Fetching environments") return config_manager.get_environments() # [/DEF:get_environments:Function] # [DEF:add_environment:Function] # @PURPOSE: Adds a new Superset environment. # @PRE: Environment data is valid and reachable. # @POST: Environment is added to config. # @PARAM: env (Environment) - The environment to add. # @RETURN: Environment - The added environment. @router.post("/environments", response_model=Environment) async def add_environment( env: Environment, config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("add_environment"): logger.info(f"[add_environment][Entry] Adding environment {env.id}") # Validate connection before adding try: client = SupersetClient(env) client.get_dashboards(query={"page_size": 1}) except Exception as e: logger.error(f"[add_environment][Coherence:Failed] Connection validation failed: {e}") raise HTTPException(status_code=400, detail=f"Connection validation failed: {e}") config_manager.add_environment(env) return env # [/DEF:add_environment:Function] # [DEF:update_environment:Function] # @PURPOSE: Updates an existing Superset environment. # @PRE: ID and valid environment data are provided. # @POST: Environment is updated in config. # @PARAM: id (str) - The ID of the environment to update. # @PARAM: env (Environment) - The updated environment data. # @RETURN: Environment - The updated environment. @router.put("/environments/{id}", response_model=Environment) async def update_environment( id: str, env: Environment, config_manager: ConfigManager = Depends(get_config_manager) ): with belief_scope("update_environment"): logger.info(f"[update_environment][Entry] Updating environment {id}") # If password is masked, we need the real one for validation env_to_validate = env.copy(deep=True) if env_to_validate.password == "********": old_env = next((e for e in config_manager.get_environments() if e.id == id), None) if old_env: env_to_validate.password = old_env.password # Validate connection before updating try: client = SupersetClient(env_to_validate) client.get_dashboards(query={"page_size": 1}) except Exception as e: logger.error(f"[update_environment][Coherence:Failed] Connection validation failed: {e}") raise HTTPException(status_code=400, detail=f"Connection validation failed: {e}") if config_manager.update_environment(id, env): return env raise HTTPException(status_code=404, detail=f"Environment {id} not found") # [/DEF:update_environment:Function] # [DEF:delete_environment:Function] # @PURPOSE: Deletes a Superset environment. # @PRE: ID is provided. # @POST: Environment is removed from config. # @PARAM: id (str) - The ID of the environment to delete. @router.delete("/environments/{id}") async def delete_environment( id: str, config_manager: ConfigManager = Depends(get_config_manager) ): with belief_scope("delete_environment"): logger.info(f"[delete_environment][Entry] Deleting environment {id}") config_manager.delete_environment(id) return {"message": f"Environment {id} deleted"} # [/DEF:delete_environment:Function] # [DEF:test_environment_connection:Function] # @PURPOSE: Tests the connection to a Superset environment. # @PRE: ID is provided. # @POST: Returns success or error status. # @PARAM: id (str) - The ID of the environment to test. # @RETURN: dict - Success message or error. @router.post("/environments/{id}/test") async def test_environment_connection( id: str, config_manager: ConfigManager = Depends(get_config_manager) ): with belief_scope("test_environment_connection"): logger.info(f"[test_environment_connection][Entry] Testing environment {id}") # Find environment env = next((e for e in config_manager.get_environments() if e.id == id), None) if not env: raise HTTPException(status_code=404, detail=f"Environment {id} not found") try: # Initialize client (this will trigger authentication) client = SupersetClient(env) # Try a simple request to verify client.get_dashboards(query={"page_size": 1}) logger.info(f"[test_environment_connection][Coherence:OK] Connection successful for {id}") return {"status": "success", "message": "Connection successful"} except Exception as e: logger.error(f"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}") return {"status": "error", "message": str(e)} # [/DEF:test_environment_connection:Function] # [DEF:get_logging_config:Function] # @PURPOSE: Retrieves current logging configuration. # @PRE: Config manager is available. # @POST: Returns logging configuration. # @RETURN: LoggingConfigResponse - The current logging config. @router.get("/logging", response_model=LoggingConfigResponse) async def get_logging_config( config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_logging_config"): logging_config = config_manager.get_config().settings.logging return LoggingConfigResponse( level=logging_config.level, task_log_level=logging_config.task_log_level, enable_belief_state=logging_config.enable_belief_state ) # [/DEF:get_logging_config:Function] # [DEF:update_logging_config:Function] # @PURPOSE: Updates logging configuration. # @PRE: New logging config is provided. # @POST: Logging configuration is updated and saved. # @PARAM: config (LoggingConfig) - The new logging configuration. # @RETURN: LoggingConfigResponse - The updated logging config. @router.patch("/logging", response_model=LoggingConfigResponse) async def update_logging_config( config: LoggingConfig, config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("update_logging_config"): logger.info(f"[update_logging_config][Entry] Updating logging config: level={config.level}, task_log_level={config.task_log_level}") # Get current settings and update logging config settings = config_manager.get_config().settings settings.logging = config config_manager.update_global_settings(settings) return LoggingConfigResponse( level=config.level, task_log_level=config.task_log_level, enable_belief_state=config.enable_belief_state ) # [/DEF:update_logging_config:Function] # [DEF:ConsolidatedSettingsResponse:Class] class ConsolidatedSettingsResponse(BaseModel): environments: List[dict] connections: List[dict] llm: dict llm_providers: List[dict] logging: dict storage: dict # [/DEF:ConsolidatedSettingsResponse:Class] # [DEF:get_consolidated_settings:Function] # @PURPOSE: Retrieves all settings categories in a single call # @PRE: Config manager is available. # @POST: Returns all consolidated settings. # @RETURN: ConsolidatedSettingsResponse - All settings categories. @router.get("/consolidated", response_model=ConsolidatedSettingsResponse) async def get_consolidated_settings( config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_consolidated_settings"): logger.info("[get_consolidated_settings][Entry] Fetching all consolidated settings") config = config_manager.get_config() from ...services.llm_provider import LLMProviderService from ...core.database import SessionLocal db = SessionLocal() try: llm_service = LLMProviderService(db) providers = llm_service.get_all_providers() llm_providers_list = [ { "id": p.id, "provider_type": p.provider_type, "name": p.name, "base_url": p.base_url, "api_key": "********", "default_model": p.default_model, "is_active": p.is_active } for p in providers ] finally: db.close() return ConsolidatedSettingsResponse( environments=[env.dict() for env in config.environments], connections=config.settings.connections, llm=config.settings.llm, llm_providers=llm_providers_list, logging=config.settings.logging.dict(), storage=config.settings.storage.dict() ) # [/DEF:get_consolidated_settings:Function] # [DEF:update_consolidated_settings:Function] # @PURPOSE: Bulk update application settings from the consolidated view. # @PRE: User has admin permissions, config is valid. # @POST: Settings are updated and saved via ConfigManager. @router.patch("/consolidated") async def update_consolidated_settings( settings_patch: dict, config_manager: ConfigManager = Depends(get_config_manager), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("update_consolidated_settings"): logger.info("[update_consolidated_settings][Entry] Applying consolidated settings patch") current_config = config_manager.get_config() current_settings = current_config.settings # Update connections if provided if "connections" in settings_patch: current_settings.connections = settings_patch["connections"] # Update LLM if provided if "llm" in settings_patch: current_settings.llm = settings_patch["llm"] # Update Logging if provided if "logging" in settings_patch: current_settings.logging = LoggingConfig(**settings_patch["logging"]) # Update Storage if provided if "storage" in settings_patch: new_storage = StorageConfig(**settings_patch["storage"]) is_valid, message = config_manager.validate_path(new_storage.root_path) if not is_valid: raise HTTPException(status_code=400, detail=message) current_settings.storage = new_storage config_manager.update_global_settings(current_settings) return {"status": "success", "message": "Settings updated"} # [/DEF:update_consolidated_settings:Function] # [/DEF:SettingsRouter:Module]