slug first logic

This commit is contained in:
2026-03-01 13:17:05 +03:00
parent f24200d52a
commit 80b28ac371
20 changed files with 1739 additions and 379 deletions

View File

@@ -155,6 +155,57 @@ class DatabaseMappingsResponse(BaseModel):
mappings: List[DatabaseMapping]
# [/DEF:DatabaseMappingsResponse:DataClass]
# [DEF:_find_dashboard_id_by_slug:Function]
# @PURPOSE: Resolve dashboard numeric ID by slug using Superset list endpoint.
# @PRE: `dashboard_slug` is non-empty.
# @POST: Returns dashboard ID when found, otherwise None.
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
# [/DEF:_find_dashboard_id_by_slug:Function]
# [DEF:_resolve_dashboard_id_from_ref:Function]
# @PURPOSE: Resolve dashboard ID from slug-first reference with numeric fallback.
# @PRE: `dashboard_ref` is provided in route path.
# @POST: Returns a valid dashboard ID or raises HTTPException(404).
def _resolve_dashboard_id_from_ref(
dashboard_ref: str,
client: SupersetClient,
) -> int:
normalized_ref = str(dashboard_ref or "").strip()
if not normalized_ref:
raise HTTPException(status_code=404, detail="Dashboard not found")
# Slug-first: even if ref looks numeric, try slug first.
slug_match_id = _find_dashboard_id_by_slug(client, normalized_ref)
if slug_match_id is not None:
return slug_match_id
if normalized_ref.isdigit():
return int(normalized_ref)
raise HTTPException(status_code=404, detail="Dashboard not found")
# [/DEF:_resolve_dashboard_id_from_ref:Function]
# [DEF:get_dashboards:Function]
# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status
# @PRE: env_id must be a valid environment ID
@@ -331,17 +382,17 @@ async def get_database_mappings(
# [DEF:get_dashboard_detail:Function]
# @PURPOSE: Fetch detailed dashboard info with related charts and datasets
# @PRE: env_id must be valid and dashboard_id must exist
# @PRE: env_id must be valid and dashboard ref (slug or id) must exist
# @POST: Returns dashboard detail payload for overview page
# @RELATION: CALLS -> SupersetClient.get_dashboard_detail
@router.get("/{dashboard_id:int}", response_model=DashboardDetailResponse)
@router.get("/{dashboard_ref}", response_model=DashboardDetailResponse)
async def get_dashboard_detail(
dashboard_id: int,
dashboard_ref: str,
env_id: str,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:migration", "READ"))
):
with belief_scope("get_dashboard_detail", f"dashboard_id={dashboard_id}, env_id={env_id}"):
with belief_scope("get_dashboard_detail", f"dashboard_ref={dashboard_ref}, env_id={env_id}"):
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
@@ -350,9 +401,10 @@ async def get_dashboard_detail(
try:
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
detail = client.get_dashboard_detail(dashboard_id)
logger.info(
f"[get_dashboard_detail][Coherence:OK] Dashboard {dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets"
f"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets"
)
return DashboardDetailResponse(**detail)
except HTTPException:
@@ -398,17 +450,38 @@ def _task_matches_dashboard(task: Any, dashboard_id: int, env_id: Optional[str])
# [DEF:get_dashboard_tasks_history:Function]
# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard.
# @PRE: dashboard_id is valid integer.
# @PRE: dashboard ref (slug or id) is valid.
# @POST: Response contains sorted task history (newest first).
@router.get("/{dashboard_id:int}/tasks", response_model=DashboardTaskHistoryResponse)
@router.get("/{dashboard_ref}/tasks", response_model=DashboardTaskHistoryResponse)
async def get_dashboard_tasks_history(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
limit: int = Query(20, ge=1, le=100),
config_manager=Depends(get_config_manager),
task_manager=Depends(get_task_manager),
_ = Depends(has_permission("tasks", "READ"))
):
with belief_scope("get_dashboard_tasks_history", f"dashboard_id={dashboard_id}, env_id={env_id}, limit={limit}"):
with belief_scope("get_dashboard_tasks_history", f"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}"):
dashboard_id: Optional[int] = None
if dashboard_ref.isdigit():
dashboard_id = int(dashboard_ref)
elif env_id:
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
else:
logger.error(
"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id"
)
raise HTTPException(
status_code=400,
detail="env_id is required when dashboard reference is a slug",
)
matching_tasks = []
for task in task_manager.get_all_tasks():
if _task_matches_dashboard(task, dashboard_id, env_id):
@@ -451,7 +524,7 @@ async def get_dashboard_tasks_history(
)
)
logger.info(f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard {dashboard_id}")
logger.info(f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}")
return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)
# [/DEF:get_dashboard_tasks_history:Function]
@@ -460,15 +533,15 @@ async def get_dashboard_tasks_history(
# @PURPOSE: Proxies Superset dashboard thumbnail with cache support.
# @PRE: env_id must exist.
# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.
@router.get("/{dashboard_id:int}/thumbnail")
@router.get("/{dashboard_ref}/thumbnail")
async def get_dashboard_thumbnail(
dashboard_id: int,
dashboard_ref: str,
env_id: str,
force: bool = Query(False),
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:migration", "READ"))
):
with belief_scope("get_dashboard_thumbnail", f"dashboard_id={dashboard_id}, env_id={env_id}, force={force}"):
with belief_scope("get_dashboard_thumbnail", f"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}"):
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
@@ -477,6 +550,7 @@ async def get_dashboard_thumbnail(
try:
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
digest = None
thumb_endpoint = None

View File

@@ -17,15 +17,18 @@ import typing
import os
from src.dependencies import get_config_manager, has_permission
from src.core.database import get_db
from src.models.git import GitServerConfig, GitRepository
from src.models.git import GitServerConfig, GitRepository, GitProvider
from src.api.routes.git_schemas import (
GitServerConfigSchema, GitServerConfigCreate,
BranchSchema, BranchCreate,
BranchCheckout, CommitSchema, CommitCreate,
DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest,
RepoStatusBatchRequest, RepoStatusBatchResponse,
GiteaRepoCreateRequest, GiteaRepoSchema,
RemoteRepoCreateRequest, RemoteRepoSchema,
)
from src.services.git_service import GitService
from src.core.superset_client import SupersetClient
from src.core.logger import logger, belief_scope
from ...services.llm_prompt_templates import (
DEFAULT_LLM_PROMPTS,
@@ -99,6 +102,78 @@ def _resolve_repository_status(dashboard_id: int) -> dict:
raise
# [/DEF:_resolve_repository_status:Function]
# [DEF:_get_git_config_or_404:Function]
# @PURPOSE: Resolve GitServerConfig by id or raise 404.
# @PRE: db session is available.
# @POST: Returns GitServerConfig model.
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
# [/DEF:_get_git_config_or_404:Function]
# [DEF:_find_dashboard_id_by_slug:Function]
# @PURPOSE: Resolve dashboard numeric ID by slug in a specific environment.
# @PRE: dashboard_slug is non-empty.
# @POST: Returns dashboard ID or None when not found.
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
# [/DEF:_find_dashboard_id_by_slug:Function]
# [DEF:_resolve_dashboard_id_from_ref:Function]
# @PURPOSE: Resolve dashboard ID from slug-or-id reference for Git routes.
# @PRE: dashboard_ref is provided; env_id is required for slug values.
# @POST: Returns numeric dashboard ID or raises HTTPException.
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
# [/DEF:_resolve_dashboard_id_from_ref:Function]
# [DEF:get_git_configs:Function]
# @PURPOSE: List all configured Git servers.
# @PRE: Database session `db` is available.
@@ -172,20 +247,175 @@ async def test_git_config(
raise HTTPException(status_code=400, detail="Connection failed")
# [/DEF:test_git_config:Function]
# [DEF:list_gitea_repositories:Function]
# @PURPOSE: List repositories in Gitea for a saved Gitea config.
# @PRE: config_id exists and provider is GITEA.
# @POST: Returns repositories visible to PAT user.
@router.get("/config/{config_id}/gitea/repos", response_model=List[GiteaRepoSchema])
async def list_gitea_repositories(
config_id: str,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "READ"))
):
with belief_scope("list_gitea_repositories"):
config = _get_git_config_or_404(db, config_id)
if config.provider != GitProvider.GITEA:
raise HTTPException(status_code=400, detail="This endpoint supports GITEA provider only")
repos = await git_service.list_gitea_repositories(config.url, config.pat)
return [
GiteaRepoSchema(
name=repo.get("name", ""),
full_name=repo.get("full_name", ""),
private=bool(repo.get("private", False)),
clone_url=repo.get("clone_url"),
html_url=repo.get("html_url"),
ssh_url=repo.get("ssh_url"),
default_branch=repo.get("default_branch"),
)
for repo in repos
]
# [/DEF:list_gitea_repositories:Function]
# [DEF:create_gitea_repository:Function]
# @PURPOSE: Create a repository in Gitea for a saved Gitea config.
# @PRE: config_id exists and provider is GITEA.
# @POST: Returns created repository payload.
@router.post("/config/{config_id}/gitea/repos", response_model=GiteaRepoSchema)
async def create_gitea_repository(
config_id: str,
request: GiteaRepoCreateRequest,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "WRITE"))
):
with belief_scope("create_gitea_repository"):
config = _get_git_config_or_404(db, config_id)
if config.provider != GitProvider.GITEA:
raise HTTPException(status_code=400, detail="This endpoint supports GITEA provider only")
repo = await git_service.create_gitea_repository(
server_url=config.url,
pat=config.pat,
name=request.name,
private=request.private,
description=request.description,
auto_init=request.auto_init,
default_branch=request.default_branch,
)
return GiteaRepoSchema(
name=repo.get("name", ""),
full_name=repo.get("full_name", ""),
private=bool(repo.get("private", False)),
clone_url=repo.get("clone_url"),
html_url=repo.get("html_url"),
ssh_url=repo.get("ssh_url"),
default_branch=repo.get("default_branch"),
)
# [/DEF:create_gitea_repository:Function]
# [DEF:create_remote_repository:Function]
# @PURPOSE: Create repository on remote Git server using selected provider config.
# @PRE: config_id exists and PAT has creation permissions.
# @POST: Returns normalized remote repository payload.
@router.post("/config/{config_id}/repositories", response_model=RemoteRepoSchema)
async def create_remote_repository(
config_id: str,
request: RemoteRepoCreateRequest,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "WRITE"))
):
with belief_scope("create_remote_repository"):
config = _get_git_config_or_404(db, config_id)
if config.provider == GitProvider.GITEA:
repo = await git_service.create_gitea_repository(
server_url=config.url,
pat=config.pat,
name=request.name,
private=request.private,
description=request.description,
auto_init=request.auto_init,
default_branch=request.default_branch,
)
elif config.provider == GitProvider.GITHUB:
repo = await git_service.create_github_repository(
server_url=config.url,
pat=config.pat,
name=request.name,
private=request.private,
description=request.description,
auto_init=request.auto_init,
default_branch=request.default_branch,
)
elif config.provider == GitProvider.GITLAB:
repo = await git_service.create_gitlab_repository(
server_url=config.url,
pat=config.pat,
name=request.name,
private=request.private,
description=request.description,
auto_init=request.auto_init,
default_branch=request.default_branch,
)
else:
raise HTTPException(status_code=501, detail=f"Provider {config.provider} is not supported")
return RemoteRepoSchema(
provider=config.provider,
name=repo.get("name", ""),
full_name=repo.get("full_name", repo.get("name", "")),
private=bool(repo.get("private", False)),
clone_url=repo.get("clone_url"),
html_url=repo.get("html_url"),
ssh_url=repo.get("ssh_url"),
default_branch=repo.get("default_branch"),
)
# [/DEF:create_remote_repository:Function]
# [DEF:delete_gitea_repository:Function]
# @PURPOSE: Delete repository in Gitea for a saved Gitea config.
# @PRE: config_id exists and provider is GITEA.
# @POST: Target repository is deleted on Gitea.
@router.delete("/config/{config_id}/gitea/repos/{owner}/{repo_name}")
async def delete_gitea_repository(
config_id: str,
owner: str,
repo_name: str,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "WRITE"))
):
with belief_scope("delete_gitea_repository"):
config = _get_git_config_or_404(db, config_id)
if config.provider != GitProvider.GITEA:
raise HTTPException(status_code=400, detail="This endpoint supports GITEA provider only")
await git_service.delete_gitea_repository(
server_url=config.url,
pat=config.pat,
owner=owner,
repo_name=repo_name,
)
return {"status": "success", "message": "Repository deleted"}
# [/DEF:delete_gitea_repository:Function]
# [DEF:init_repository:Function]
# @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init.
# @PRE: `dashboard_id` exists and `init_data` contains valid config_id and remote_url.
# @PRE: `dashboard_ref` exists and `init_data` contains valid config_id and remote_url.
# @POST: Repository is initialized on disk and a GitRepository record is saved in DB.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: init_data (RepoInitRequest)
@router.post("/repositories/{dashboard_id}/init")
@router.post("/repositories/{dashboard_ref}/init")
async def init_repository(
dashboard_id: int,
dashboard_ref: str,
init_data: RepoInitRequest,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("init_repository"):
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
# 1. Get config
config = db.query(GitServerConfig).filter(GitServerConfig.id == init_data.config_id).first()
if not config:
@@ -225,17 +455,20 @@ async def init_repository(
# [DEF:get_branches:Function]
# @PURPOSE: List all branches for a dashboard's repository.
# @PRE: Repository for `dashboard_id` is initialized.
# @PRE: Repository for `dashboard_ref` is initialized.
# @POST: Returns a list of branches from the local repository.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @RETURN: List[BranchSchema]
@router.get("/repositories/{dashboard_id}/branches", response_model=List[BranchSchema])
@router.get("/repositories/{dashboard_ref}/branches", response_model=List[BranchSchema])
async def get_branches(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("get_branches"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return git_service.list_branches(dashboard_id)
except HTTPException:
raise
@@ -245,18 +478,21 @@ async def get_branches(
# [DEF:create_branch:Function]
# @PURPOSE: Create a new branch in the dashboard's repository.
# @PRE: `dashboard_id` repository exists and `branch_data` has name and from_branch.
# @PRE: `dashboard_ref` repository exists and `branch_data` has name and from_branch.
# @POST: A new branch is created in the local repository.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: branch_data (BranchCreate)
@router.post("/repositories/{dashboard_id}/branches")
@router.post("/repositories/{dashboard_ref}/branches")
async def create_branch(
dashboard_id: int,
dashboard_ref: str,
branch_data: BranchCreate,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("create_branch"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch)
return {"status": "success"}
except HTTPException:
@@ -267,18 +503,21 @@ async def create_branch(
# [DEF:checkout_branch:Function]
# @PURPOSE: Switch the dashboard's repository to a specific branch.
# @PRE: `dashboard_id` repository exists and branch `checkout_data.name` exists.
# @PRE: `dashboard_ref` repository exists and branch `checkout_data.name` exists.
# @POST: The local repository HEAD is moved to the specified branch.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: checkout_data (BranchCheckout)
@router.post("/repositories/{dashboard_id}/checkout")
@router.post("/repositories/{dashboard_ref}/checkout")
async def checkout_branch(
dashboard_id: int,
dashboard_ref: str,
checkout_data: BranchCheckout,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("checkout_branch"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
git_service.checkout_branch(dashboard_id, checkout_data.name)
return {"status": "success"}
except HTTPException:
@@ -289,18 +528,21 @@ async def checkout_branch(
# [DEF:commit_changes:Function]
# @PURPOSE: Stage and commit changes in the dashboard's repository.
# @PRE: `dashboard_id` repository exists and `commit_data` has message and files.
# @PRE: `dashboard_ref` repository exists and `commit_data` has message and files.
# @POST: Specified files are staged and a new commit is created.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: commit_data (CommitCreate)
@router.post("/repositories/{dashboard_id}/commit")
@router.post("/repositories/{dashboard_ref}/commit")
async def commit_changes(
dashboard_id: int,
dashboard_ref: str,
commit_data: CommitCreate,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("commit_changes"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files)
return {"status": "success"}
except HTTPException:
@@ -311,16 +553,19 @@ async def commit_changes(
# [DEF:push_changes:Function]
# @PURPOSE: Push local commits to the remote repository.
# @PRE: `dashboard_id` repository exists and has a remote configured.
# @PRE: `dashboard_ref` repository exists and has a remote configured.
# @POST: Local commits are pushed to the remote repository.
# @PARAM: dashboard_id (int)
@router.post("/repositories/{dashboard_id}/push")
# @PARAM: dashboard_ref (str)
@router.post("/repositories/{dashboard_ref}/push")
async def push_changes(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("push_changes"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
git_service.push_changes(dashboard_id)
return {"status": "success"}
except HTTPException:
@@ -331,16 +576,19 @@ async def push_changes(
# [DEF:pull_changes:Function]
# @PURPOSE: Pull changes from the remote repository.
# @PRE: `dashboard_id` repository exists and has a remote configured.
# @PRE: `dashboard_ref` repository exists and has a remote configured.
# @POST: Remote changes are fetched and merged into the local branch.
# @PARAM: dashboard_id (int)
@router.post("/repositories/{dashboard_id}/pull")
# @PARAM: dashboard_ref (str)
@router.post("/repositories/{dashboard_ref}/pull")
async def pull_changes(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("pull_changes"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
git_service.pull_changes(dashboard_id)
return {"status": "success"}
except HTTPException:
@@ -351,18 +599,21 @@ async def pull_changes(
# [DEF:sync_dashboard:Function]
# @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin.
# @PRE: `dashboard_id` is valid; GitPlugin is available.
# @PRE: `dashboard_ref` is valid; GitPlugin is available.
# @POST: Dashboard YAMLs are exported from Superset and committed to Git.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: source_env_id (Optional[str])
@router.post("/repositories/{dashboard_id}/sync")
@router.post("/repositories/{dashboard_ref}/sync")
async def sync_dashboard(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
source_env_id: typing.Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("sync_dashboard"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
from src.plugins.git_plugin import GitPlugin
plugin = GitPlugin()
return await plugin.execute({
@@ -400,18 +651,21 @@ async def get_environments(
# [DEF:deploy_dashboard:Function]
# @PURPOSE: Deploy dashboard from Git to a target environment.
# @PRE: `dashboard_id` and `deploy_data.environment_id` are valid.
# @PRE: `dashboard_ref` and `deploy_data.environment_id` are valid.
# @POST: Dashboard YAMLs are read from Git and imported into the target Superset.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: deploy_data (DeployRequest)
@router.post("/repositories/{dashboard_id}/deploy")
@router.post("/repositories/{dashboard_ref}/deploy")
async def deploy_dashboard(
dashboard_id: int,
dashboard_ref: str,
deploy_data: DeployRequest,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("deploy_dashboard"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
from src.plugins.git_plugin import GitPlugin
plugin = GitPlugin()
return await plugin.execute({
@@ -427,19 +681,22 @@ async def deploy_dashboard(
# [DEF:get_history:Function]
# @PURPOSE: View commit history for a dashboard's repository.
# @PRE: `dashboard_id` repository exists.
# @PRE: `dashboard_ref` repository exists.
# @POST: Returns a list of recent commits from the repository.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: limit (int)
# @RETURN: List[CommitSchema]
@router.get("/repositories/{dashboard_id}/history", response_model=List[CommitSchema])
@router.get("/repositories/{dashboard_ref}/history", response_model=List[CommitSchema])
async def get_history(
dashboard_id: int,
dashboard_ref: str,
limit: int = 50,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("get_history"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return git_service.get_commit_history(dashboard_id, limit)
except HTTPException:
raise
@@ -449,17 +706,20 @@ async def get_history(
# [DEF:get_repository_status:Function]
# @PURPOSE: Get current Git status for a dashboard repository.
# @PRE: `dashboard_id` is a valid integer.
# @PRE: `dashboard_ref` resolves to a valid dashboard.
# @POST: Returns repository status; if repo is not initialized, returns `NO_REPO` payload.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @RETURN: dict
@router.get("/repositories/{dashboard_id}/status")
@router.get("/repositories/{dashboard_ref}/status")
async def get_repository_status(
dashboard_id: int,
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("get_repository_status"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
return _resolve_repository_status(dashboard_id)
except HTTPException:
raise
@@ -513,21 +773,24 @@ async def get_repository_status_batch(
# [DEF:get_repository_diff:Function]
# @PURPOSE: Get Git diff for a dashboard repository.
# @PRE: `dashboard_id` repository exists.
# @PRE: `dashboard_ref` repository exists.
# @POST: Returns the diff text for the specified file or all changes.
# @PARAM: dashboard_id (int)
# @PARAM: dashboard_ref (str)
# @PARAM: file_path (Optional[str])
# @PARAM: staged (bool)
# @RETURN: str
@router.get("/repositories/{dashboard_id}/diff")
@router.get("/repositories/{dashboard_ref}/diff")
async def get_repository_diff(
dashboard_id: int,
dashboard_ref: str,
file_path: Optional[str] = None,
staged: bool = False,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("get_repository_diff"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
diff_text = git_service.get_diff(dashboard_id, file_path, staged)
return diff_text
except HTTPException:
@@ -538,17 +801,19 @@ async def get_repository_diff(
# [DEF:generate_commit_message:Function]
# @PURPOSE: Generate a suggested commit message using LLM.
# @PRE: Repository for `dashboard_id` is initialized.
# @PRE: Repository for `dashboard_ref` is initialized.
# @POST: Returns a suggested commit message string.
@router.post("/repositories/{dashboard_id}/generate-message")
@router.post("/repositories/{dashboard_ref}/generate-message")
async def generate_commit_message(
dashboard_id: int,
db: Session = Depends(get_db),
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager = Depends(get_config_manager),
db: Session = Depends(get_db),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("generate_commit_message"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
# 1. Get Diff
diff = git_service.get_diff(dashboard_id, staged=True)
if not diff:

View File

@@ -154,4 +154,53 @@ class RepoStatusBatchResponse(BaseModel):
statuses: Dict[str, Dict[str, Any]]
# [/DEF:RepoStatusBatchResponse:Class]
# [DEF:GiteaRepoSchema:Class]
# @PURPOSE: Schema describing a Gitea repository.
class GiteaRepoSchema(BaseModel):
name: str
full_name: str
private: bool = False
clone_url: Optional[str] = None
html_url: Optional[str] = None
ssh_url: Optional[str] = None
default_branch: Optional[str] = None
# [/DEF:GiteaRepoSchema:Class]
# [DEF:GiteaRepoCreateRequest:Class]
# @PURPOSE: Request schema for creating a Gitea repository.
class GiteaRepoCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
private: bool = True
description: Optional[str] = None
auto_init: bool = True
default_branch: Optional[str] = "main"
# [/DEF:GiteaRepoCreateRequest:Class]
# [DEF:RemoteRepoSchema:Class]
# @PURPOSE: Provider-agnostic remote repository payload.
class RemoteRepoSchema(BaseModel):
provider: GitProvider
name: str
full_name: str
private: bool = False
clone_url: Optional[str] = None
html_url: Optional[str] = None
ssh_url: Optional[str] = None
default_branch: Optional[str] = None
# [/DEF:RemoteRepoSchema:Class]
# [DEF:RemoteRepoCreateRequest:Class]
# @PURPOSE: Provider-agnostic repository creation request.
class RemoteRepoCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
private: bool = True
description: Optional[str] = None
auto_init: bool = True
default_branch: Optional[str] = "main"
# [/DEF:RemoteRepoCreateRequest:Class]
# [/DEF:backend.src.api.routes.git_schemas:Module]