Files
ss-tools/backend/src/api/routes/git/_helpers.py
busya 306c5ae742 semantics: complete DEF-to-region migration, fix regressions
- Convert legacy [DEF🆔Type] anchors to #region/#endregion across 329 files
- Reinstate _normalize_timestamp_value in sql_generator.py
- Fix MarkerLogger→logger migration in events.py (molecular CoT markers)
- Fix dataset_review orchestrator dependencies (_build_execution_snapshot)
- Fix config_manager stale-record deletion (moved to save path only)
- Add 77 missing [/DEF:] closers in 5 unbalanced test files
- Update assistant_chat.integration.test.js for #region format
- Apply molecular-cot-logging markers (REASON/REFLECT/EXPLORE) via logger.* methods
2026-05-12 23:54:55 +03:00

332 lines
11 KiB
Python

# #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