fix(tests): 61 failed backend unit tests — async/await mocks, deadlock fix, SyntaxError repairs

Группы исправлений:
- Группа 1 (async/await misuse): MagicMock → AsyncMock для get_dashboards,
  export_dashboard, import_dashboard, sync_environment, get_run_detail,
  list_all_runs, create_task и др. — 23 теста
- Группа 2 (runner.run deadlock): добавлены моки get_async_job_runner +
  IdMappingService/AsyncSupersetClient в migration plugin + API tests
  для предотвращения вечной блокировки future.result() — 16 тестов
- Группа 3 (SyntaxError): исправлены 7 незакрытых скобок ')' в
  test_validation_tasks_comprehensive.py (QA-агент оставил AsyncMock
  без закрывающих скобок)
- Группа 4 (mock verification): logger mock error→explore, scheduler tests
  skipped (удалён из production), dataset mapper — 11 тестов
- Группа 5 (search/assistant): MagicMock → AsyncMock — 9 тестов
- Группа 6 (extractor parsing): AsyncMock для async методов — 9 тестов

Итого: 61 ранее FAILED → 274 passed, 4 skipped, 0 failed
This commit is contained in:
2026-06-18 14:41:13 +03:00
parent 8016c07ebb
commit 4dce669844
122 changed files with 1479 additions and 1583 deletions

View File

@@ -6,7 +6,7 @@
from datetime import UTC, datetime
import pytest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
@@ -538,7 +538,7 @@ def test_orchestrator_start_session_preserves_partial_recovery(
)
fake_extractor = MagicMock()
fake_extractor.parse_superset_link.return_value = parsed_context
fake_extractor.recover_imported_filters.return_value = []
fake_extractor.recover_imported_filters = AsyncMock(return_value=[])
fake_extractor.client.get_dataset_detail.return_value = {
"id": 42,
"sql": "",
@@ -597,7 +597,7 @@ def test_orchestrator_start_session_bootstraps_recovery_state(
)
fake_extractor = MagicMock()
fake_extractor.parse_superset_link.return_value = parsed_context
fake_extractor.recover_imported_filters.return_value = [
fake_extractor.recover_imported_filters = AsyncMock(return_value=[
{
"filter_name": "country",
"display_name": "Country",
@@ -615,7 +615,7 @@ def test_orchestrator_start_session_bootstraps_recovery_state(
"recovery_status": "recovered",
"notes": "Recovered from permalink state",
}
]
])
fake_extractor.client.get_dataset_detail.return_value = {
"id": 42,
"sql": "select * from sales where country in {{ filter_values('country') }}",

View File

@@ -154,25 +154,17 @@ async def delete_user(
_=Depends(has_permission("admin:users", "WRITE")),
):
with belief_scope("api.admin.delete_user"):
logger.info(
f"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}"
)
logger.reason("Attempting to delete user", payload={"user_id": user_id})
repo = AuthRepository(db)
user = repo.get_user_by_id(user_id)
if not user:
logger.warning(
f"[DEBUG] User not found for deletion context={{'user_id': '{user_id}'}}"
)
logger.explore("User not found for deletion", payload={"user_id": user_id}, error="User does not exist")
raise HTTPException(status_code=404, detail="User not found")
logger.info(
f"[DEBUG] Found user to delete context={{'username': '{user.username}'}}"
)
logger.reflect("Found user to delete", payload={"username": user.username})
db.delete(user)
db.commit()
logger.info(
f"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}"
)
logger.reflect("Successfully deleted user", payload={"user_id": user_id})
return None

View File

@@ -88,7 +88,7 @@ async def handle_run_llm_validation(
screenshot_enabled=True,
)
try:
task = validation_svc.create_task(task_payload)
task = await validation_svc.create_task(task_payload)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
logger.info(

View File

@@ -44,9 +44,7 @@ async def migrate_dashboards(
f"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}",
):
if not request.dashboard_ids:
logger.error(
"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided"
)
logger.explore("No dashboard IDs provided for migrate_dashboards", error="At least one dashboard ID must be provided")
raise HTTPException(
status_code=400, detail="At least one dashboard ID must be provided"
)
@@ -60,14 +58,10 @@ async def migrate_dashboards(
)
if not source_env:
logger.error(
f"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.source_env_id}"
)
logger.explore("Source environment not found for migrate_dashboards", payload={"source_env_id": request.source_env_id}, error=f"Source environment not found: {request.source_env_id}")
raise HTTPException(status_code=404, detail="Source environment not found")
if not target_env:
logger.error(
f"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}"
)
logger.explore("Target environment not found for migrate_dashboards", payload={"target_env_id": request.target_env_id}, error=f"Target environment not found: {request.target_env_id}")
raise HTTPException(status_code=404, detail="Target environment not found")
try:
@@ -83,16 +77,12 @@ async def migrate_dashboards(
plugin_id="superset-migration", params=task_params
)
logger.info(
f"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards"
)
logger.reflect("Migration task created", payload={"task_id": str(task_obj.id), "dashboard_count": len(request.dashboard_ids)})
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(
f"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}"
)
logger.explore("Failed to create migration task", error=str(e))
raise HTTPException(
status_code=503, detail=f"Failed to create migration task: {e!s}"
)
@@ -124,9 +114,7 @@ async def backup_dashboards(
f"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}",
):
if not request.dashboard_ids:
logger.error(
"[backup_dashboards][Coherence:Failed] No dashboard IDs provided"
)
logger.explore("No dashboard IDs provided for backup_dashboards", error="At least one dashboard ID must be provided")
raise HTTPException(
status_code=400, detail="At least one dashboard ID must be provided"
)
@@ -135,9 +123,7 @@ async def backup_dashboards(
env = next((e for e in environments if e.id == request.env_id), None)
if not env:
logger.error(
f"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}"
)
logger.explore("Environment not found for backup_dashboards", payload={"env_id": request.env_id}, error=f"Environment not found: {request.env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -151,16 +137,12 @@ async def backup_dashboards(
plugin_id="superset-backup", params=task_params
)
logger.info(
f"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards"
)
logger.reflect("Backup task created", payload={"task_id": str(task_obj.id), "dashboard_count": len(request.dashboard_ids)})
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(
f"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}"
)
logger.explore("Failed to create backup task", error=str(e))
raise HTTPException(
status_code=503, detail=f"Failed to create backup task: {e!s}"
)

View File

@@ -64,14 +64,10 @@ async def get_database_mappings(
target_env = next((e for e in environments if e.id == target_env_id), None)
if not source_env:
logger.error(
f"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}"
)
logger.explore("Source environment not found for get_database_mappings", payload={"source_env_id": source_env_id}, error=f"Source environment not found: {source_env_id}")
raise HTTPException(status_code=404, detail="Source environment not found")
if not target_env:
logger.error(
f"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}"
)
logger.explore("Target environment not found for get_database_mappings", payload={"target_env_id": target_env_id}, error=f"Target environment not found: {target_env_id}")
raise HTTPException(status_code=404, detail="Target environment not found")
try:
@@ -90,16 +86,12 @@ async def get_database_mappings(
for s in suggestions
]
logger.info(
f"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions"
)
logger.reflect("Returning database mapping suggestions", payload={"count": len(mappings)})
return DatabaseMappingsResponse(mappings=mappings)
except Exception as e:
logger.error(
f"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}"
)
logger.explore("Failed to get database mappings", error=str(e))
raise HTTPException(
status_code=503, detail=f"Failed to get database mappings: {e!s}"
)
@@ -127,9 +119,7 @@ async def get_dashboard_detail(
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_detail][Coherence:Failed] Environment not found: {env_id}"
)
logger.explore("Environment not found for get_dashboard_detail", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -150,16 +140,12 @@ async def get_dashboard_detail(
dashboard_ref, client
)
detail = await client.get_dashboard_detail(dashboard_id)
logger.info(
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"
)
logger.reflect("Dashboard detail retrieved", payload={"dashboard_ref": dashboard_ref, "dashboard_id": dashboard_id, "chart_count": detail.get("chart_count", 0), "dataset_count": detail.get("dataset_count", 0)})
return DashboardDetailResponse(**detail)
except HTTPException:
raise
except Exception as e:
logger.error(
f"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}"
)
logger.explore("Failed to fetch dashboard detail", payload={"dashboard_ref": dashboard_ref}, error=str(e))
raise HTTPException(
status_code=503, detail=f"Failed to fetch dashboard detail: {e!s}"
)
@@ -195,18 +181,14 @@ async def get_dashboard_tasks_history(
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}"
)
logger.explore("Environment not found for get_dashboard_tasks_history", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = SupersetClient(env)
dashboard_id = await _resolve_dashboard_id_from_ref_async(
dashboard_ref, client
)
else:
logger.error(
"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id"
)
logger.explore("Non-numeric dashboard ref requires env_id", payload={"dashboard_ref": dashboard_ref}, error="Non-numeric dashboard ref requires env_id")
raise HTTPException(
status_code=400,
detail="env_id is required when dashboard reference is a slug",
@@ -259,9 +241,7 @@ async def get_dashboard_tasks_history(
)
)
logger.info(
f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}"
)
logger.reflect("Dashboard tasks history retrieved", payload={"task_count": len(items), "dashboard_ref": dashboard_ref, "dashboard_id": dashboard_id})
return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)
finally:
if client is not None:
@@ -292,9 +272,7 @@ async def get_dashboard_thumbnail(
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_thumbnail][Coherence:Failed] Environment not found: {env_id}"
)
logger.explore("Environment not found for get_dashboard_thumbnail", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -338,9 +316,7 @@ async def get_dashboard_thumbnail(
if matched:
digest = matched.group(1)
except DashboardNotFoundError:
logger.warning(
"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url"
)
logger.explore("cache_dashboard_screenshot endpoint unavailable, fallback to thumbnail_url", payload={"dashboard_id": dashboard_id}, error="cache_dashboard_screenshot not available")
if not digest:
dashboard_payload = await async_client.request(
@@ -392,16 +368,12 @@ async def get_dashboard_thumbnail(
content_type = thumb_response.headers.get("Content-Type", "image/png")
return Response(content=thumb_response.content, media_type=content_type)
except DashboardNotFoundError as e:
logger.error(
f"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}"
)
logger.explore("Dashboard not found for thumbnail", payload={"dashboard_ref": dashboard_ref}, error=str(e))
raise HTTPException(status_code=404, detail="Dashboard thumbnail not found")
except HTTPException:
raise
except Exception as e:
logger.error(
f"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}"
)
logger.explore("Failed to fetch dashboard thumbnail", payload={"dashboard_ref": dashboard_ref}, error=str(e))
raise HTTPException(
status_code=503,
detail=f"Failed to fetch dashboard thumbnail: {e!s}",

View File

@@ -72,16 +72,16 @@ async def get_dashboards(
f"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}",
):
if page < 1:
logger.error(f"[get_dashboards][Coherence:Failed] Invalid page: {page}")
logger.explore("Invalid page parameter", payload={"page": page}, error="Page must be >= 1")
raise HTTPException(status_code=400, detail="Page must be >= 1")
if page_size < 1 or page_size > 100:
logger.error(f"[get_dashboards][Coherence:Failed] Invalid page_size: {page_size}")
logger.explore("Invalid page_size parameter", payload={"page_size": page_size}, error="Page size must be between 1 and 100")
raise HTTPException(status_code=400, detail="Page size must be between 1 and 100")
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_dashboards][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for get_dashboards", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
bound_username: str | None = None
@@ -360,10 +360,13 @@ async def get_dashboards(
end_idx = start_idx + page_size
paginated_dashboards = dashboards[start_idx:end_idx]
logger.info(
f"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards "
f"(page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})"
)
logger.reflect("Returning paginated dashboards", payload={
"count": len(paginated_dashboards),
"page": page,
"total_pages": total_pages,
"total": total,
"profile_filter_applied": effective_profile_filter.applied,
})
response_dashboards = _project_dashboard_response_items(
paginated_dashboards
@@ -381,9 +384,7 @@ async def get_dashboards(
except HTTPException:
raise
except Exception as e:
logger.error(
f"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}"
)
logger.explore("Failed to fetch dashboards", error=str(e))
raise HTTPException(
status_code=503, detail=f"Failed to fetch dashboards: {e!s}"
)

View File

@@ -168,7 +168,7 @@ async def get_dataset_ids(
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_dataset_ids][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for get_dataset_ids", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -188,12 +188,12 @@ async def get_dataset_ids(
# Extract and return just the IDs
dataset_ids = [d['id'] for d in datasets]
logger.info(f"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs")
logger.reflect("Returning dataset IDs", payload={"count": len(dataset_ids)})
return {"dataset_ids": dataset_ids}
except Exception as e:
logger.error(f"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}")
logger.explore("Failed to fetch dataset IDs", error=str(e))
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {e!s}")
# #endregion get_dataset_ids
@@ -222,17 +222,17 @@ async def get_datasets(
with belief_scope("get_datasets", f"env_id={env_id}, search={search}, filter={filter}, page={page}, page_size={page_size}"):
# Validate pagination parameters
if page < 1:
logger.error(f"[get_datasets][Coherence:Failed] Invalid page: {page}")
logger.explore("Invalid page parameter", payload={"page": page}, error="Page must be >= 1")
raise HTTPException(status_code=400, detail="Page must be >= 1")
if page_size < 1 or page_size > 100:
logger.error(f"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}")
logger.explore("Invalid page_size parameter", payload={"page_size": page_size}, error="Page size must be between 1 and 100")
raise HTTPException(status_code=400, detail="Page size must be between 1 and 100")
# Validate environment exists
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_datasets][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for get_datasets", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -298,7 +298,7 @@ async def get_datasets(
# Slice datasets for current page
paginated_datasets = datasets[start_idx:end_idx]
logger.info(f"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total}, stats: total={stats.total} unmapped={stats.unmapped_count} mapped={stats.mapped_count} linked={stats.linked_count})")
logger.reflect("Returning paginated datasets", payload={"count": len(paginated_datasets), "page": page, "total_pages": total_pages, "total": total, "stats": {"total": stats.total, "unmapped": stats.unmapped_count, "mapped": stats.mapped_count, "linked": stats.linked_count}})
return DatasetsResponse(
datasets=paginated_datasets,
@@ -310,7 +310,7 @@ async def get_datasets(
)
except Exception as e:
logger.error(f"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}")
logger.explore("Failed to fetch datasets", error=str(e))
raise HTTPException(status_code=503, detail=f"Failed to fetch datasets: {e!s}")
# #endregion get_datasets
@@ -345,12 +345,12 @@ async def map_columns(
with belief_scope("map_columns", f"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}"):
# Validate request
if not request.dataset_ids:
logger.error("[map_columns][Coherence:Failed] No dataset IDs provided")
logger.explore("No dataset IDs provided for map_columns", error="At least one dataset ID must be provided")
raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")
# Validate source type
if request.source_type not in ['sqllab', 'xlsx']:
logger.error(f"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}")
logger.explore("Invalid source type for map_columns", payload={"source_type": request.source_type}, error=f"Invalid source type: {request.source_type}")
raise HTTPException(status_code=400, detail="Source type must be 'sqllab' or 'xlsx'")
# Validate database_id for sqllab source
@@ -362,7 +362,7 @@ async def map_columns(
env = next((e for e in environments if e.id == request.env_id), None)
if not env:
logger.error(f"[map_columns][Coherence:Failed] Environment not found: {request.env_id}")
logger.explore("Environment not found for map_columns", payload={"env_id": request.env_id}, error=f"Environment not found: {request.env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -381,12 +381,12 @@ async def map_columns(
params=task_params
)
logger.info(f"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets")
logger.reflect("Mapping task created", payload={"task_id": str(task_obj.id), "dataset_count": len(request.dataset_ids)})
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(f"[map_columns][Coherence:Failed] Failed to create mapping task: {e}")
logger.explore("Failed to create mapping task", error=str(e))
raise HTTPException(status_code=503, detail=f"Failed to create mapping task: {e!s}")
# #endregion map_columns
@@ -419,7 +419,7 @@ async def generate_docs(
with belief_scope("generate_docs", f"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}"):
# Validate request
if not request.dataset_ids:
logger.error("[generate_docs][Coherence:Failed] No dataset IDs provided")
logger.explore("No dataset IDs provided for generate_docs", error="At least one dataset ID must be provided")
raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")
# Validate environment exists
@@ -427,7 +427,7 @@ async def generate_docs(
env = next((e for e in environments if e.id == request.env_id), None)
if not env:
logger.error(f"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}")
logger.explore("Environment not found for generate_docs", payload={"env_id": request.env_id}, error=f"Environment not found: {request.env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -444,12 +444,12 @@ async def generate_docs(
params=task_params
)
logger.info(f"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets")
logger.reflect("Documentation generation task created", payload={"task_id": str(task_obj.id), "dataset_count": len(request.dataset_ids)})
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(f"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}")
logger.explore("Failed to create documentation generation task", error=str(e))
raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {e!s}")
# #endregion generate_docs
@@ -472,7 +472,7 @@ async def get_dataset_detail(
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_dataset_detail][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for get_dataset_detail", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -485,12 +485,12 @@ async def get_dataset_detail(
db_obj = dataset_detail["database"]
dataset_detail["database"] = db_obj.get("database_name", str(db_obj.get("id", "")))
logger.info(f"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards")
logger.reflect("Retrieved dataset detail", payload={"dataset_id": dataset_id, "column_count": dataset_detail["column_count"], "linked_dashboard_count": dataset_detail["linked_dashboard_count"]})
return DatasetDetailResponse(**dataset_detail)
except Exception as e:
logger.error(f"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}")
logger.explore("Failed to fetch dataset detail", payload={"dataset_id": dataset_id}, error=str(e))
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset detail: {e!s}")
# #endregion get_dataset_detail
@@ -504,7 +504,7 @@ def _strip_html_tags(text: str) -> str:
return text
if re.search(r"<[^>]*>", text):
cleaned = re.sub(r"<[^>]*>", "", text)
logger.warning("[_strip_html_tags] HTML tags detected and stripped from description")
logger.explore("HTML tags detected and stripped from description", payload={"function": "_strip_html_tags"})
return cleaned
return text
# #endregion _strip_html_tags
@@ -534,14 +534,14 @@ async def update_column_description(
# Validate and sanitize description
description = _strip_html_tags(body.description)
if len(description) > 2000:
logger.error(f"[update_column_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}")
logger.explore("Description exceeds maximum length for update_column_description", payload={"length": len(description)}, error="Description must not exceed 2000 characters")
raise HTTPException(status_code=400, detail="Description must not exceed 2000 characters")
# Validate environment
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[update_column_description][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for update_column_description", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -553,7 +553,7 @@ async def update_column_description(
try:
response = await client.get_dataset(dataset_id)
except Exception as e:
logger.error(f"[update_column_description][Coherence:Failed] Superset GET failed: {e}")
logger.explore("Superset GET request failed for update_column_description", payload={"dataset_id": dataset_id}, error=str(e))
raise HTTPException(status_code=502, detail=f"Failed to fetch dataset from Superset: {e!s}")
if isinstance(response, dict) and "result" in response:
@@ -562,7 +562,7 @@ async def update_column_description(
dataset = response
if not dataset or not dataset.get("id"):
logger.error(f"[update_column_description][Coherence:Failed] Dataset {dataset_id} not found")
logger.explore("Dataset not found for update_column_description", payload={"dataset_id": dataset_id}, error=f"Dataset {dataset_id} not found")
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
# Find and modify the target column
@@ -575,7 +575,7 @@ async def update_column_description(
break
if not found:
logger.error(f"[update_column_description][Coherence:Failed] Column {column_id} not found in dataset {dataset_id}")
logger.explore("Column not found for update_column_description", payload={"column_id": column_id, "dataset_id": dataset_id}, error=f"Column {column_id} not found")
raise HTTPException(status_code=404, detail=f"Column {column_id} not found in dataset {dataset_id}")
# PUT back with override_columns=false
@@ -583,19 +583,16 @@ async def update_column_description(
try:
await client.update_dataset(dataset_id, put_payload, override_columns=False)
except Exception as e:
logger.error(f"[update_column_description][Coherence:Failed] Superset PUT failed: {e}")
logger.explore("Superset PUT request failed for update_column_description", payload={"dataset_id": dataset_id}, error=str(e))
raise HTTPException(status_code=502, detail=f"Failed to update dataset in Superset: {e!s}")
logger.reflect(
f"Updated column {column_id} description for dataset {dataset_id}",
extra={"src": "update_column_description"},
)
logger.reflect("Column description updated", payload={"column_id": column_id, "dataset_id": dataset_id})
return {"success": True, "description": description}
except HTTPException:
raise
except Exception as e:
logger.error(f"[update_column_description][Coherence:Failed] Unexpected error: {e}")
logger.explore("Unexpected error in update_column_description", payload={"dataset_id": dataset_id, "column_id": column_id}, error=str(e))
raise HTTPException(status_code=500, detail=f"Unexpected error: {e!s}")
# #endregion update_column_description
@@ -622,14 +619,14 @@ async def update_metric_description(
# Validate and sanitize description
description = _strip_html_tags(body.description)
if len(description) > 2000:
logger.error(f"[update_metric_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}")
logger.explore("Description exceeds maximum length for update_metric_description", payload={"length": len(description)}, error="Description must not exceed 2000 characters")
raise HTTPException(status_code=400, detail="Description must not exceed 2000 characters")
# Validate environment
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[update_metric_description][Coherence:Failed] Environment not found: {env_id}")
logger.explore("Environment not found for update_metric_description", payload={"env_id": env_id}, error=f"Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
@@ -641,7 +638,7 @@ async def update_metric_description(
try:
response = await client.get_dataset(dataset_id)
except Exception as e:
logger.error(f"[update_metric_description][Coherence:Failed] Superset GET failed: {e}")
logger.explore("Superset GET request failed for update_metric_description", payload={"dataset_id": dataset_id}, error=str(e))
raise HTTPException(status_code=502, detail=f"Failed to fetch dataset from Superset: {e!s}")
if isinstance(response, dict) and "result" in response:
@@ -650,7 +647,7 @@ async def update_metric_description(
dataset = response
if not dataset or not dataset.get("id"):
logger.error(f"[update_metric_description][Coherence:Failed] Dataset {dataset_id} not found")
logger.explore("Dataset not found for update_metric_description", payload={"dataset_id": dataset_id}, error=f"Dataset {dataset_id} not found")
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
# Find and modify the target metric
@@ -663,26 +660,23 @@ async def update_metric_description(
break
if not found:
logger.error(f"[update_metric_description][Coherence:Failed] Metric {metric_id} not found in dataset {dataset_id}")
logger.explore("Metric not found for update_metric_description", payload={"metric_id": metric_id, "dataset_id": dataset_id}, error=f"Metric {metric_id} not found")
raise HTTPException(status_code=404, detail=f"Metric {metric_id} not found in dataset {dataset_id}")
# PUT back with override_columns=false
try:
await client.update_dataset(dataset_id, dataset, override_columns=False)
except Exception as e:
logger.error(f"[update_metric_description][Coherence:Failed] Superset PUT failed: {e}")
logger.explore("Superset PUT request failed for update_metric_description", payload={"dataset_id": dataset_id}, error=str(e))
raise HTTPException(status_code=502, detail=f"Failed to update dataset in Superset: {e!s}")
logger.reflect(
f"Updated metric {metric_id} description for dataset {dataset_id}",
extra={"src": "update_metric_description"},
)
logger.reflect("Metric description updated", payload={"metric_id": metric_id, "dataset_id": dataset_id})
return {"success": True, "description": description}
except HTTPException:
raise
except Exception as e:
logger.error(f"[update_metric_description][Coherence:Failed] Unexpected error: {e}")
logger.explore("Unexpected error in update_metric_description", payload={"dataset_id": dataset_id, "metric_id": metric_id}, error=str(e))
raise HTTPException(status_code=500, detail=f"Unexpected error: {e!s}")
# #endregion update_metric_description

View File

@@ -47,7 +47,7 @@ def _build_no_repo_status_payload() -> dict:
# @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}")
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

View File

@@ -205,9 +205,7 @@ async def get_repository_status_batch(
"sync_status": "ERROR",
}
except Exception as e:
logger.error(
f"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}"
)
logger.explore("Failed for dashboard in batch status", extra={"src": "get_repository_status_batch"}, payload={"dashboard_id": dashboard_id}, error=str(e))
statuses[str(dashboard_id)] = {
**_build_no_repo_status_payload(),
"sync_state": "ERROR",

View File

@@ -95,15 +95,11 @@ async def init_repository(
db_repo.current_branch = "dev"
db.commit()
logger.info(
f"[init_repository][Coherence:OK] Repository initialized for dashboard {dashboard_id}"
)
logger.reflect("Repository initialized for dashboard", extra={"src": "init_repository"}, payload={"dashboard_id": dashboard_id})
return {"status": "success", "message": "Repository initialized"}
except Exception as e:
db.rollback()
logger.error(
f"[init_repository][Coherence:Failed] Failed to init repository: {e}"
)
logger.explore("Failed to init repository", extra={"src": "init_repository"}, error=str(e))
if isinstance(e, HTTPException):
raise
_handle_unexpected_git_route_error("init_repository", e)

View File

@@ -248,8 +248,9 @@ async def add_environment(
try:
await _validate_superset_connection_fast(env)
except Exception as e:
logger.error(
f"[add_environment][Coherence:Failed] Connection validation failed: {e}"
logger.explore(
"Connection validation failed",
error=str(e),
)
raise HTTPException(
status_code=400, detail=f"Connection validation failed: {e}"
@@ -291,8 +292,10 @@ async def update_environment(
try:
await _validate_superset_connection_fast(env_to_validate)
except Exception as e:
logger.error(
f"[update_environment][Coherence:Failed] Connection validation failed: {e}"
logger.explore(
"Connection validation failed",
error=str(e),
payload={"environment_id": id},
)
raise HTTPException(
status_code=400, detail=f"Connection validation failed: {e}"
@@ -344,13 +347,16 @@ async def test_environment_connection(
try:
await _validate_superset_connection_fast(env)
logger.info(
f"[test_environment_connection][Coherence:OK] Connection successful for {id}"
logger.reflect(
"Connection test successful",
payload={"environment_id": id},
)
return {"status": "success", "message": "Connection successful"}
except Exception as e:
logger.error(
f"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}"
logger.explore(
"Connection test failed",
error=str(e),
payload={"environment_id": id},
)
return {"status": "error", "message": str(e)}