# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS git, helpers, resolution, identity] # @BRIEF Shared helper functions for Git route modules. # @LAYER: API # @RELATION USES -> [GitDeps] # @RELATION USES -> [SupersetClient] # @RELATION USES -> [UserDashboardPreference] import os from typing import Optional from fastapi import HTTPException from sqlalchemy.orm import Session from src.core.logger import logger from src.core.superset_client import SupersetClient from src.dependencies import get_config_manager from src.models.auth import User from src.models.git import GitServerConfig from src.models.profile import UserDashboardPreference from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH # #region _build_no_repo_status_payload [C:1] [TYPE Function] # @BRIEF Build a consistent status payload for dashboards without initialized repositories. # @POST: Returns a stable payload compatible with frontend repository status parsing. def _build_no_repo_status_payload() -> dict: return { "is_dirty": False, "untracked_files": [], "modified_files": [], "staged_files": [], "current_branch": None, "upstream_branch": None, "has_upstream": False, "ahead_count": 0, "behind_count": 0, "is_diverged": False, "sync_state": "NO_REPO", "sync_status": "NO_REPO", "has_repo": False, } # #endregion _build_no_repo_status_payload # #region _handle_unexpected_git_route_error [C:1] [TYPE Function] # @BRIEF Convert unexpected route-level exceptions to stable 500 API responses. # @PRE: `error` is a non-HTTPException instance. # @POST: Raises HTTPException(500) with route-specific context. def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None: logger.error(f"[{route_name}][Coherence:Failed] {error}") raise HTTPException(status_code=500, detail=f"{route_name} failed: {str(error)}") # #endregion _handle_unexpected_git_route_error # #region _resolve_repository_status [C:2] [TYPE Function] # @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics. # @PRE: `dashboard_id` is a valid integer. # @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent. def _resolve_repository_status(dashboard_id: int) -> dict: git_service = get_git_service() repo_path = git_service._get_repo_path(dashboard_id) if not os.path.exists(repo_path): logger.debug( f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}" ) return _build_no_repo_status_payload() try: return git_service.get_status(dashboard_id) except HTTPException as e: if e.status_code == 404: logger.debug( f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}" ) return _build_no_repo_status_payload() raise # #endregion _resolve_repository_status # #region _get_git_config_or_404 [C:2] [TYPE Function] # @BRIEF Resolve GitServerConfig by id or raise 404. def _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig: config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first() if not config: raise HTTPException(status_code=404, detail="Git configuration not found") return config # #endregion _get_git_config_or_404 # #region _find_dashboard_id_by_slug [C:2] [TYPE Function] # @BRIEF Resolve dashboard numeric ID by slug in a specific environment. def _find_dashboard_id_by_slug( client: SupersetClient, dashboard_slug: str, ) -> Optional[int]: query_variants = [ { "filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1, }, { "filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1, }, ] for query in query_variants: try: _count, dashboards = client.get_dashboards_page(query=query) if dashboards: resolved_id = dashboards[0].get("id") if resolved_id is not None: return int(resolved_id) except Exception: continue return None # #endregion _find_dashboard_id_by_slug # #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function] # @BRIEF Resolve dashboard ID from slug-or-id reference for Git routes. def _resolve_dashboard_id_from_ref( dashboard_ref: str, config_manager, env_id: Optional[str] = None, ) -> int: normalized_ref = str(dashboard_ref or "").strip() if not normalized_ref: raise HTTPException(status_code=400, detail="dashboard_ref is required") if normalized_ref.isdigit(): return int(normalized_ref) if not env_id: raise HTTPException( status_code=400, detail="env_id is required for slug-based Git operations", ) environments = config_manager.get_environments() env = next((e for e in environments if e.id == env_id), None) if not env: raise HTTPException(status_code=404, detail="Environment not found") dashboard_id = _find_dashboard_id_by_slug(SupersetClient(env), normalized_ref) if dashboard_id is None: raise HTTPException( status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found" ) return dashboard_id # #endregion _resolve_dashboard_id_from_ref # #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function] # @BRIEF Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes. async def _find_dashboard_id_by_slug_async( client: "AsyncSupersetClient", dashboard_slug: str, ) -> Optional[int]: query_variants = [ { "filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1, }, { "filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1, }, ] for query in query_variants: try: _count, dashboards = await client.get_dashboards_page_async(query=query) if dashboards: resolved_id = dashboards[0].get("id") if resolved_id is not None: return int(resolved_id) except Exception: continue return None # #endregion _find_dashboard_id_by_slug_async # #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function] # @BRIEF Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes. async def _resolve_dashboard_id_from_ref_async( dashboard_ref: str, config_manager, env_id: Optional[str] = None, ) -> int: normalized_ref = str(dashboard_ref or "").strip() if not normalized_ref: raise HTTPException(status_code=400, detail="dashboard_ref is required") if normalized_ref.isdigit(): return int(normalized_ref) if not env_id: raise HTTPException( status_code=400, detail="env_id is required for slug-based Git operations", ) environments = config_manager.get_environments() env = next((e for e in environments if e.id == env_id), None) if not env: raise HTTPException(status_code=404, detail="Environment not found") from src.core.async_superset_client import AsyncSupersetClient client = AsyncSupersetClient(env) try: dashboard_id = await _find_dashboard_id_by_slug_async(client, normalized_ref) if dashboard_id is None: raise HTTPException( status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found" ) return dashboard_id finally: await client.aclose() # #endregion _resolve_dashboard_id_from_ref_async # #region _resolve_repo_key_from_ref [C:2] [TYPE Function] # @BRIEF Resolve repository folder key with slug-first strategy and deterministic fallback. def _resolve_repo_key_from_ref( dashboard_ref: str, dashboard_id: int, config_manager, env_id: Optional[str] = None, ) -> str: normalized_ref = str(dashboard_ref or "").strip() if normalized_ref and not normalized_ref.isdigit(): return normalized_ref if env_id: try: environments = config_manager.get_environments() env = next((e for e in environments if e.id == env_id), None) if env: payload = SupersetClient(env).get_dashboard(dashboard_id) dashboard_data = ( payload.get("result", payload) if isinstance(payload, dict) else {} ) dashboard_slug = dashboard_data.get("slug") if dashboard_slug: return str(dashboard_slug) except Exception: pass return f"dashboard-{dashboard_id}" # #endregion _resolve_repo_key_from_ref # #region _sanitize_optional_identity_value [C:1] [TYPE Function] # @BRIEF Normalize optional identity value into trimmed string or None. def _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]: normalized = str(value or "").strip() if not normalized: return None return normalized # #endregion _sanitize_optional_identity_value # #region _resolve_current_user_git_identity [C:2] [TYPE Function] # @BRIEF Resolve configured Git username/email from current user's profile preferences. def _resolve_current_user_git_identity( db: Session, current_user: Optional[User], ) -> Optional[tuple[str, str]]: if db is None or not hasattr(db, "query"): return None user_id = _sanitize_optional_identity_value(getattr(current_user, "id", None)) if not user_id: return None try: preference = ( db.query(UserDashboardPreference) .filter(UserDashboardPreference.user_id == user_id) .first() ) except Exception as resolve_error: logger.warning( "[_resolve_current_user_git_identity][Action] Failed to load profile preference for user %s: %s", user_id, resolve_error, ) return None if not preference: return None git_username = _sanitize_optional_identity_value( getattr(preference, "git_username", None) ) git_email = _sanitize_optional_identity_value( getattr(preference, "git_email", None) ) if not git_username or not git_email: return None return git_username, git_email # #endregion _resolve_current_user_git_identity # #region _apply_git_identity_from_profile [C:2] [TYPE Function] # @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations. def _apply_git_identity_from_profile( dashboard_id: int, db: Session, current_user: Optional[User], ) -> None: identity = _resolve_current_user_git_identity(db, current_user) if not identity: return git_service = get_git_service() configure_identity = getattr(git_service, "configure_identity", None) if not callable(configure_identity): return git_username, git_email = identity configure_identity(dashboard_id, git_username, git_email) # #endregion _apply_git_identity_from_profile # #endregion GitHelpers