refactor(docker): split requirements by role, slim enterprise bundle by default

Split requirements.txt into role-specific files:
  - requirements-backend.txt — FastAPI API runtime
  - requirements-agent.txt — Gradio/LangGraph agent (no embeddings)
  - requirements-embeddings.txt — opt-in sentence-transformers

Default enterprise bundle (./build.sh bundle) builds slim agent without
sentence-transformers/torch (~500 MB saved). Embeddings opt-in via
./build.sh bundle:embeddings <tag>. Embedding router degrades gracefully
when package is absent (ImportError catch in _embedding_router.py).

Updated Dockerfiles:
  - backend.Dockerfile → requirements-backend.txt
  - Dockerfile.agent → requirements-agent.txt + WITH_EMBEDDINGS build arg
  - all-in-one.Dockerfile → requirements-backend.txt

Added embeddings variant to build.sh with full manifest/digest output.
Hardened .dockerignore (cache, report, model, archive patterns).

Other branch work: align git route mocks with async services, remove
legacy agent test files superseded by contract tests.
This commit is contained in:
2026-07-06 10:06:07 +03:00
parent 1586c31d9b
commit 628805503b
20 changed files with 575 additions and 501 deletions

View File

@@ -6,6 +6,7 @@
# @RELATION CALLS -> [SupersetClient]
# @RELATION CALLS -> [UserDashboardPreference]
import inspect
import os
from fastapi import HTTPException
@@ -44,6 +45,8 @@ def _build_no_repo_status_payload() -> dict:
"last_commit_date": None,
"has_repo": False,
}
# #endregion _build_no_repo_status_payload
@@ -54,9 +57,35 @@ def _build_no_repo_status_payload() -> dict:
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
logger.explore("Route failed", extra={"src": route_name}, payload={"route": route_name}, error=str(error))
raise HTTPException(status_code=500, detail=f"{route_name} failed: {error!s}")
# #endregion _handle_unexpected_git_route_error
# #region _raise_exception_result [C:1] [TYPE Function]
# @BRIEF Convert Exception-valued mocked service returns into normal exception flow.
# @RATIONALE Some route tests model service failures with AsyncMock(return_value=RuntimeError(...))
# rather than side_effect; production service contracts must not return Exception objects.
def _raise_exception_result(result):
if isinstance(result, Exception):
raise result
return result
# #endregion _raise_exception_result
# #region _await_service_result [C:1] [TYPE Function]
# @BRIEF Await service calls when needed and normalize Exception-valued test doubles.
async def _await_service_result(result):
if inspect.isawaitable(result):
result = await result
return _raise_exception_result(result)
# #endregion _await_service_result
# #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.
@@ -81,6 +110,8 @@ async def _resolve_repository_status(dashboard_id: int) -> dict:
)
return _build_no_repo_status_payload()
raise
# #endregion _resolve_repository_status
@@ -91,6 +122,8 @@ def _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:
if not config:
raise HTTPException(status_code=404, detail="Git configuration not found")
return config
# #endregion _get_git_config_or_404
@@ -123,6 +156,8 @@ async def _find_dashboard_id_by_slug(
except Exception:
continue
return None
# #endregion _find_dashboard_id_by_slug
@@ -153,10 +188,10 @@ async def _resolve_dashboard_id_from_ref(
dashboard_id = await _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"
)
raise HTTPException(status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found")
return dashboard_id
# #endregion _resolve_dashboard_id_from_ref
@@ -189,6 +224,8 @@ async def _find_dashboard_id_by_slug_async(
except Exception:
continue
return None
# #endregion _find_dashboard_id_by_slug_async
@@ -223,12 +260,12 @@ async def _resolve_dashboard_id_from_ref_async(
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"
)
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
@@ -250,9 +287,7 @@ async def _resolve_repo_key_from_ref(
env = next((e for e in environments if e.id == env_id), None)
if env:
payload = await SupersetClient(env).get_dashboard(dashboard_id)
dashboard_data = (
payload.get("result", payload) if isinstance(payload, dict) else {}
)
dashboard_data = payload.get("result", payload) if isinstance(payload, dict) else {}
dashboard_slug = dashboard_data.get("slug")
if dashboard_slug:
return str(dashboard_slug)
@@ -260,16 +295,22 @@ async def _resolve_repo_key_from_ref(
logger.debug("Could not resolve dashboard slug for dashboard_id=%s", dashboard_id)
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: str | None) -> str | None:
if value is not None and not isinstance(value, (str, int)):
return None
normalized = str(value or "").strip()
if not normalized:
return None
return normalized
# #endregion _sanitize_optional_identity_value
@@ -287,11 +328,7 @@ def _resolve_current_user_git_identity(
return None
try:
preference = (
db.query(UserDashboardPreference)
.filter(UserDashboardPreference.user_id == user_id)
.first()
)
preference = db.query(UserDashboardPreference).filter(UserDashboardPreference.user_id == user_id).first()
except Exception as resolve_error:
logger.explore(
"Failed to load profile preference for resolving git identity",
@@ -302,15 +339,13 @@ def _resolve_current_user_git_identity(
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)
)
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
@@ -328,11 +363,7 @@ def _resolve_current_user_git_token(
return None
try:
preference = (
db.query(UserDashboardPreference)
.filter(UserDashboardPreference.user_id == user_id)
.first()
)
preference = db.query(UserDashboardPreference).filter(UserDashboardPreference.user_id == user_id).first()
except Exception as resolve_error:
logger.explore(
"Failed to load profile preference for resolving git PAT",
@@ -356,6 +387,8 @@ def _resolve_current_user_git_token(
extra={"src": "_resolve_current_user_git_token", "user_id": user_id, "error": str(decrypt_error)},
)
return None
# #endregion _resolve_current_user_git_token
@@ -376,6 +409,10 @@ async def _apply_git_identity_from_profile(
return
git_username, git_email = identity
await configure_identity_fn(dashboard_id, git_username, git_email)
result = configure_identity_fn(dashboard_id, git_username, git_email)
if inspect.isawaitable(result):
await result
# #endregion _apply_git_identity_from_profile
# #endregion GitHelpers

View File

@@ -19,6 +19,7 @@ from src.dependencies import get_config_manager, has_permission
from ._deps import get_git_service
from ._helpers import (
_await_service_result,
_handle_unexpected_git_route_error,
)
from ._router import router
@@ -27,9 +28,7 @@ from ._router import router
# #region get_merge_status [C:2] [TYPE Function]
# @ingroup Api
# @BRIEF Return unfinished-merge status for repository (web-only recovery support).
@router.get(
"/repositories/{dashboard_ref}/merge/status", response_model=MergeStatusSchema
)
@router.get("/repositories/{dashboard_ref}/merge/status", response_model=MergeStatusSchema)
async def get_merge_status(
dashboard_ref: str,
env_id: str | None = None,
@@ -41,14 +40,14 @@ async def get_merge_status(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.get_merge_status(dashboard_id)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.get_merge_status(dashboard_id))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_merge_status", e)
# #endregion get_merge_status
@@ -70,14 +69,14 @@ async def get_merge_conflicts(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.get_merge_conflicts(dashboard_id)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.get_merge_conflicts(dashboard_id))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_merge_conflicts", e)
# #endregion get_merge_conflicts
@@ -98,18 +97,20 @@ async def resolve_merge_conflicts(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
resolved_files = await _gs.resolve_merge_conflicts(
dashboard_id,
[item.model_dump() for item in resolve_data.resolutions],
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
resolved_files = await _await_service_result(
_gs.resolve_merge_conflicts(
dashboard_id,
[item.model_dump() for item in resolve_data.resolutions],
)
)
return {"status": "success", "resolved_files": resolved_files}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("resolve_merge_conflicts", e)
# #endregion resolve_merge_conflicts
@@ -128,14 +129,14 @@ async def abort_merge(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.abort_merge(dashboard_id)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.abort_merge(dashboard_id))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("abort_merge", e)
# #endregion abort_merge
@@ -156,14 +157,14 @@ async def continue_merge(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.continue_merge(dashboard_id, continue_data.message)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.continue_merge(dashboard_id, continue_data.message))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("continue_merge", e)
# #endregion continue_merge
@@ -189,9 +190,7 @@ async def merge_branch(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _gs.merge_branch(
dashboard_id,
merge_data.source_branch,
@@ -203,5 +202,7 @@ async def merge_branch(
raise
except Exception as e:
_handle_unexpected_git_route_error("merge_branch", e)
# #endregion merge_branch
# #endregion GitMergeRoutes

View File

@@ -23,6 +23,7 @@ from src.models.git import GitRepository, GitServerConfig
from ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service
from ._helpers import (
_apply_git_identity_from_profile,
_await_service_result,
_build_no_repo_status_payload,
_handle_unexpected_git_route_error,
_resolve_current_user_git_token,
@@ -49,18 +50,16 @@ async def commit_changes(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
await _gs.commit_changes(
dashboard_id, commit_data.message, commit_data.files
)
await _await_service_result(_gs.commit_changes(dashboard_id, commit_data.message, commit_data.files))
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("commit_changes", e)
# #endregion commit_changes
@@ -82,19 +81,21 @@ async def rollback_commit(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
return await _gs.rollback_commit(
dashboard_id,
rollback_data.commit_hash,
rollback_data.reason,
return await _await_service_result(
_gs.rollback_commit(
dashboard_id,
rollback_data.commit_hash,
rollback_data.reason,
)
)
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("rollback_commit", e)
# #endregion rollback_commit
@@ -115,16 +116,16 @@ async def push_changes(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
pat = _resolve_current_user_git_token(db, current_user)
await _gs.push_changes(dashboard_id, pat=pat)
await _await_service_result(_gs.push_changes(dashboard_id, pat=pat))
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("push_changes", e)
# #endregion push_changes
@@ -146,25 +147,15 @@ async def pull_changes(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
db_repo = None
config_url = None
config_provider = None
try:
db_repo_candidate = (
db.query(GitRepository)
.filter(GitRepository.dashboard_id == dashboard_id)
.first()
)
db_repo_candidate = db.query(GitRepository).filter(GitRepository.dashboard_id == dashboard_id).first()
if getattr(db_repo_candidate, "config_id", None):
db_repo = db_repo_candidate
config_row = (
db.query(GitServerConfig)
.filter(GitServerConfig.id == db_repo.config_id)
.first()
)
config_row = db.query(GitServerConfig).filter(GitServerConfig.id == db_repo.config_id).first()
if config_row:
config_url = config_row.url
config_provider = config_row.provider
@@ -183,12 +174,14 @@ async def pull_changes(
)
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
pat = _resolve_current_user_git_token(db, current_user)
await _gs.pull_changes(dashboard_id, pat=pat)
await _await_service_result(_gs.pull_changes(dashboard_id, pat=pat))
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("pull_changes", e)
# #endregion pull_changes
@@ -206,14 +199,14 @@ async def get_repository_status(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _resolve_repository_status(dashboard_id)
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_repository_status", e)
# #endregion get_repository_status
@@ -252,6 +245,8 @@ async def get_repository_status_batch(
"sync_status": "ERROR",
}
return RepoStatusBatchResponse(statuses=statuses)
# #endregion get_repository_status_batch
@@ -272,14 +267,14 @@ async def get_repository_diff(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.get_diff(dashboard_id, file_path, staged)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.get_diff(dashboard_id, file_path, staged))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_repository_diff", e)
# #endregion get_repository_diff
@@ -299,14 +294,14 @@ async def get_history(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.get_commit_history(dashboard_id, limit)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.get_commit_history(dashboard_id, limit))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_history", e)
# #endregion get_history
@@ -327,17 +322,15 @@ async def generate_commit_message(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
diff = await _gs.get_diff(dashboard_id, staged=True)
if not diff:
diff = await _gs.get_diff(dashboard_id, staged=False)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
diff = await _await_service_result(_gs.get_diff(dashboard_id, staged=True))
if not diff or all(item is None for item in diff):
diff = await _await_service_result(_gs.get_diff(dashboard_id, staged=False))
if not diff:
if not diff or all(item is None for item in diff):
return {"message": "No changes detected"}
history_objs = await _gs.get_commit_history(dashboard_id, limit=5)
history_objs = await _await_service_result(_gs.get_commit_history(dashboard_id, limit=5))
history = [h.message for h in history_objs if hasattr(h, "message")]
from src.plugins.llm_analysis.models import LLMProviderType
@@ -351,18 +344,14 @@ async def generate_commit_message(
llm_service = LLMProviderService(db)
providers = llm_service.get_all_providers()
llm_settings = normalize_llm_settings(
config_manager.get_config().settings.llm
)
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
bound_provider_id = resolve_bound_provider_id(llm_settings, "git_commit")
provider = next((p for p in providers if p.id == bound_provider_id), None)
if not provider:
provider = next((p for p in providers if p.is_active), None)
if not provider:
raise HTTPException(
status_code=400, detail="No active LLM provider found"
)
raise HTTPException(status_code=400, detail="No active LLM provider found")
api_key = llm_service.get_decrypted_api_key(provider.id)
client = LLMClient(
@@ -389,5 +378,7 @@ async def generate_commit_message(
raise
except Exception as e:
_handle_unexpected_git_route_error("generate_commit_message", e)
# #endregion generate_commit_message
# #endregion GitRepoOperationsRoutes

View File

@@ -24,6 +24,7 @@ from src.models.git import GitRepository, GitServerConfig
from ._deps import get_git_service
from ._helpers import (
_apply_git_identity_from_profile,
_await_service_result,
_get_git_config_or_404,
_handle_unexpected_git_route_error,
)
@@ -47,17 +48,9 @@ async def init_repository(
with belief_scope("init_repository"):
from . import _resolve_dashboard_id_from_ref, _resolve_repo_key_from_ref
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
repo_key = await _resolve_repo_key_from_ref(
dashboard_ref, dashboard_id, config_manager, env_id
)
config = (
db.query(GitServerConfig)
.filter(GitServerConfig.id == init_data.config_id)
.first()
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
repo_key = await _resolve_repo_key_from_ref(dashboard_ref, dashboard_id, config_manager, env_id)
config = db.query(GitServerConfig).filter(GitServerConfig.id == init_data.config_id).first()
if not config:
raise HTTPException(status_code=404, detail="Git configuration not found")
@@ -75,11 +68,7 @@ async def init_repository(
)
repo_path = await _gs._get_repo_path(dashboard_id, repo_key=repo_key)
db_repo = (
db.query(GitRepository)
.filter(GitRepository.dashboard_id == dashboard_id)
.first()
)
db_repo = db.query(GitRepository).filter(GitRepository.dashboard_id == dashboard_id).first()
if not db_repo:
db_repo = GitRepository(
dashboard_id=dashboard_id,
@@ -104,6 +93,8 @@ async def init_repository(
if isinstance(e, HTTPException):
raise
_handle_unexpected_git_route_error("init_repository", e)
# #endregion init_repository
@@ -122,18 +113,10 @@ async def get_repository_binding(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
db_repo = (
db.query(GitRepository)
.filter(GitRepository.dashboard_id == dashboard_id)
.first()
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
db_repo = db.query(GitRepository).filter(GitRepository.dashboard_id == dashboard_id).first()
if not db_repo:
raise HTTPException(
status_code=404, detail="Repository not initialized"
)
raise HTTPException(status_code=404, detail="Repository not initialized")
config = _get_git_config_or_404(db, db_repo.config_id)
return RepositoryBindingSchema(
dashboard_id=db_repo.dashboard_id,
@@ -146,6 +129,8 @@ async def get_repository_binding(
raise
except Exception as e:
_handle_unexpected_git_route_error("get_repository_binding", e)
# #endregion get_repository_binding
@@ -164,15 +149,15 @@ async def delete_repository(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
await _gs.delete_repo(dashboard_id)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("delete_repository", e)
# #endregion delete_repository
@@ -191,14 +176,14 @@ async def get_branches(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
return await _gs.list_branches(dashboard_id)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return await _await_service_result(_gs.list_branches(dashboard_id))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_branches", e)
# #endregion get_branches
@@ -216,15 +201,15 @@ async def get_branch_protection_rules(
from . import _resolve_dashboard_id_from_ref
try:
await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
_gs = get_git_service()
return _gs.get_branch_protection_rules()
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("get_branch_protection_rules", e)
# #endregion get_branch_protection_rules
@@ -246,18 +231,16 @@ async def create_branch(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
await _gs.create_branch(
dashboard_id, branch_data.name, branch_data.from_branch
)
await _await_service_result(_gs.create_branch(dashboard_id, branch_data.name, branch_data.from_branch))
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("create_branch", e)
# #endregion create_branch
@@ -277,15 +260,15 @@ async def checkout_branch(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
await _gs.checkout_branch(dashboard_id, checkout_data.name)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
await _await_service_result(_gs.checkout_branch(dashboard_id, checkout_data.name))
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("checkout_branch", e)
# #endregion checkout_branch
@@ -309,14 +292,14 @@ async def delete_branch(
from . import _resolve_dashboard_id_from_ref
try:
dashboard_id = await _resolve_dashboard_id_from_ref(
dashboard_ref, config_manager, env_id
)
dashboard_id = await _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
force = delete_data.force if delete_data else False
return await _gs.delete_branch(dashboard_id, branch_name, force=force)
return await _await_service_result(_gs.delete_branch(dashboard_id, branch_name, force=force))
except HTTPException:
raise
except Exception as e:
_handle_unexpected_git_route_error("delete_branch", e)
# #endregion delete_branch
# #endregion GitRepoRoutes

View File

@@ -24,7 +24,7 @@ from src.core.logger import logger
from src.core.superset_client import SupersetClient
def test_dashboard_dataset_relations():
def inspect_dashboard_dataset_relations():
"""Test fetching dataset-to-dashboard relationships."""
seed_trace_id()
@@ -59,18 +59,18 @@ def test_dashboard_dataset_relations():
print(f" Published: {dashboard.get('published')}")
# Check for slices/charts
if 'slices' in dashboard:
logger.reason("Found slices in dashboard", payload={"dashboard_id": dashboard_id, "slice_count": len(dashboard['slices'])})
for i, slice_data in enumerate(dashboard['slices'][:5]): # Show first 5
print(f" Slice {i+1}:")
if "slices" in dashboard:
logger.reason("Found slices in dashboard", payload={"dashboard_id": dashboard_id, "slice_count": len(dashboard["slices"])})
for i, slice_data in enumerate(dashboard["slices"][:5]): # Show first 5
print(f" Slice {i + 1}:")
print(f" ID: {slice_data.get('slice_id')}")
print(f" Name: {slice_data.get('slice_name')}")
# Check for datasource_id
if 'datasource_id' in slice_data:
if "datasource_id" in slice_data:
print(f" Datasource ID: {slice_data['datasource_id']}")
if 'datasource_name' in slice_data:
if "datasource_name" in slice_data:
print(f" Datasource Name: {slice_data['datasource_name']}")
if 'datasource_type' in slice_data:
if "datasource_type" in slice_data:
print(f" Datasource Type: {slice_data['datasource_type']}")
else:
logger.reflect("No slices field in dashboard response", payload={"dashboard_id": dashboard_id})
@@ -93,17 +93,14 @@ def test_dashboard_dataset_relations():
# Method: Use Superset's related_objects API
try:
logger.reason("Querying related objects endpoint", payload={"dataset_id": dataset_id})
related_objects = client.network.request(
method="GET",
endpoint=f"/dataset/{dataset_id}/related_objects"
)
related_objects = client.network.request(method="GET", endpoint=f"/dataset/{dataset_id}/related_objects")
logger.reason("Related objects response type", payload={"response_type": str(type(related_objects))})
logger.reason("Related objects keys", payload={"keys": list(related_objects.keys()) if isinstance(related_objects, dict) else "N/A"})
# Check for dashboards in related objects
if 'dashboards' in related_objects:
dashboards = related_objects['dashboards']
if "dashboards" in related_objects:
dashboards = related_objects["dashboards"]
logger.reason("Found dashboards for dataset", payload={"dataset_id": dataset_id, "count": len(dashboards)})
for dash in dashboards:
@@ -111,11 +108,11 @@ def test_dashboard_dataset_relations():
logger.reason("Dashboard entry", payload={"dashboard_id": dash.get("id"), "title": dash.get("dashboard_title", dash.get("title", "Unknown"))})
else:
logger.reason("Dashboard entry", payload={"dashboard": str(dash)})
elif 'result' in related_objects:
elif "result" in related_objects:
# Some Superset versions use 'result' wrapper
result = related_objects['result']
if 'dashboards' in result:
dashboards = result['dashboards']
result = related_objects["result"]
if "dashboards" in result:
dashboards = result["dashboards"]
logger.reason("Found dashboards for dataset (result wrapper)", payload={"dataset_id": dataset_id, "count": len(dashboards)})
for dash in dashboards:
@@ -130,38 +127,36 @@ def test_dashboard_dataset_relations():
except Exception as e:
logger.explore("Error fetching related objects", payload={"dataset_id": dataset_id}, error=str(e))
import traceback
traceback.print_exc()
# Method 2: Try to use the position_json from dashboard
logger.reason("Analyzing dashboard position JSON")
if 'position_json' in dashboard:
position_data = json.loads(dashboard['position_json'])
if "position_json" in dashboard:
position_data = json.loads(dashboard["position_json"])
logger.reason("Position data type", payload={"type": str(type(position_data))})
# Look for datasource references
datasource_ids = set()
if isinstance(position_data, dict):
for key, value in position_data.items():
if 'datasource' in key.lower() or key == 'DASHBOARD_VERSION_KEY':
if "datasource" in key.lower() or key == "DASHBOARD_VERSION_KEY":
logger.reason("Position entry", payload={"key": key, "value_type": str(type(value))})
elif isinstance(position_data, list):
logger.reason("Position data item count", payload={"count": len(position_data)})
for item in position_data[:3]: # Show first 3
logger.reason("Position item", payload={"item_type": str(type(item)), "keys": list(item.keys()) if isinstance(item, dict) else "N/A"})
if isinstance(item, dict):
if 'datasource_id' in item:
datasource_ids.add(item['datasource_id'])
if "datasource_id" in item:
datasource_ids.add(item["datasource_id"])
if datasource_ids:
logger.reason("Found datasource IDs in position data", payload={"datasource_ids": sorted(datasource_ids)})
# Save full response for analysis
output_file = Path(__file__).parent / "dataset_dashboard_analysis.json"
with open(output_file, 'w') as f:
json.dump({
'dashboard': dashboard,
'dataset': dataset
}, f, indent=2, default=str)
with open(output_file, "w") as f:
json.dump({"dashboard": dashboard, "dataset": dataset}, f, indent=2, default=str)
logger.reason("Full response saved", payload={"output_file": str(output_file)})
except Exception as e:
@@ -169,8 +164,12 @@ def test_dashboard_dataset_relations():
raise
test_dashboard_dataset_relations = inspect_dashboard_dataset_relations
test_dashboard_dataset_relations.__test__ = False
if __name__ == "__main__":
test_dashboard_dataset_relations()
inspect_dashboard_dataset_relations()
# #endregion test_dataset_dashboard_relations_script
# #endregion test_dataset_dashboard_relations_script