fix(translate): исправление INSERT + выгрузка CSV для неуспешных запусков
**Исправления ошибок:**
- superset_executor: убран несуществующий fallback endpoint /api/v1/sql_lab/execute/
(правильный URL /api/v1/sqllab/execute/ подтверждён браузерным запросом Superset)
Ошибка 500 (permission denied) больше не маскируется 404
- orchestrator_run_completion: run.status=FAILED при провале insert
(раньше всегда ставился COMPLETED). timeout тоже treated as failure
- test_orchestrator: assertion обновлён COMPLETED→FAILED
**Новая функция — выгрузка CSV:**
- GET /api/translate/runs/{run_id}/failed.csv — возвращает CSV с успешно
переведёнными, но не вставленными записями (status=SUCCESS)
- Колонки: record_id, source_*, target_sql, source_data (JSON), языки, error_message
- Метаданные запуска (run_status, insert_status, run_error) в конце файла
- Кнопки скачивания в 3 местах фронта:
- История запусков (при status=FAILED)
- TranslationRunProgress (insert_failed)
- TranslationRunResult (failed/partial/insert_failed)
- downloadFailedCsv() через fetchApiBlob + Blob URL
This commit is contained in:
@@ -420,7 +420,7 @@ class TestTranslationOrchestrator:
|
||||
# endregion test_execute_run_no_job
|
||||
|
||||
# region test_execute_run_insert_failure [TYPE Function]
|
||||
# @BRIEF SQL generation/insert returns failure, run still completes but with error logged.
|
||||
# @BRIEF SQL generation/insert returns failure — run marked FAILED with error logged.
|
||||
def test_execute_run_insert_failure(self, mock_job: MagicMock) -> None:
|
||||
db = MagicMock()
|
||||
config_manager = MagicMock()
|
||||
@@ -464,7 +464,7 @@ class TestTranslationOrchestrator:
|
||||
patch.object(engine, 'event_log'):
|
||||
result = orch.execute_run(run)
|
||||
|
||||
assert result.status == "COMPLETED"
|
||||
assert result.status == "FAILED"
|
||||
assert result.insert_status == "failed"
|
||||
assert "timeout" in (result.error_message or "")
|
||||
|
||||
|
||||
@@ -106,9 +106,17 @@ def complete_success(
|
||||
run.insert_status = insert_result.get("status")
|
||||
run.superset_execution_id = str(insert_result.get("query_id") or "")
|
||||
run.superset_execution_log = insert_result
|
||||
if run.status != "FAILED":
|
||||
# Treat "failed" and "timeout" as terminal failures.
|
||||
# "skipped" (no records) and "success" are non-failure outcomes.
|
||||
if insert_result.get("status") in ("failed", "timeout"):
|
||||
run.status = "FAILED"
|
||||
if insert_result.get("error_message"):
|
||||
run.error_message = insert_result["error_message"]
|
||||
elif not run.error_message:
|
||||
run.error_message = f"SQL insert phase {insert_result.get('status')}"
|
||||
elif run.status != "FAILED":
|
||||
run.status = "COMPLETED"
|
||||
if insert_result.get("error_message"):
|
||||
if insert_result.get("error_message") and not run.error_message:
|
||||
run.error_message = insert_result["error_message"]
|
||||
run.completed_at = datetime.now(UTC)
|
||||
db.flush()
|
||||
|
||||
@@ -173,36 +173,30 @@ class SupersetSqlLabExecutor:
|
||||
"client_id": f"trl-{uuid.uuid4().hex[:7]}", # max 11 chars for Superset varchar(11)
|
||||
}
|
||||
|
||||
# Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,
|
||||
# others use /sql_lab/execute/ (with underscore)
|
||||
candidate_endpoints = ["/api/v1/sqllab/execute/", "/api/v1/sql_lab/execute/"]
|
||||
response = None
|
||||
last_error = None
|
||||
for endpoint in candidate_endpoints:
|
||||
try:
|
||||
response = client.network.request(
|
||||
method="POST",
|
||||
endpoint=endpoint,
|
||||
data=json.dumps(payload),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
if isinstance(response, dict) and response:
|
||||
logger.reason("SQL Lab endpoint succeeded", extra={
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
break
|
||||
except Exception as ep_err:
|
||||
last_error = ep_err
|
||||
logger.explore("SQL Lab endpoint failed, trying next", extra={
|
||||
"error": str(ep_err),
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
response = None
|
||||
|
||||
if response is None:
|
||||
error_msg = f"All SQL Lab endpoints failed. Last error: {last_error}"
|
||||
logger.explore("SQL Lab execute failed", extra={"error": error_msg})
|
||||
raise ValueError(f"Superset SQL Lab execute failed: {error_msg}")
|
||||
# Use the canonical Superset SQL Lab execute endpoint.
|
||||
# NOTE: Some older docs reference /sql_lab/execute/ (with underscore),
|
||||
# but this endpoint does NOT exist in current Superset versions.
|
||||
# The browser UI uses /api/v1/sqllab/execute/ — confirmed by
|
||||
# fetch() calls in Superset SQL Lab.
|
||||
endpoint = "/api/v1/sqllab/execute/"
|
||||
try:
|
||||
response = client.network.request(
|
||||
method="POST",
|
||||
endpoint=endpoint,
|
||||
data=json.dumps(payload),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
logger.reason("SQL Lab endpoint succeeded", extra={
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
except Exception as ep_err:
|
||||
logger.explore("SQL Lab execute failed", extra={
|
||||
"error": str(ep_err),
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
raise ValueError(
|
||||
f"Superset SQL Lab execute failed: {ep_err}"
|
||||
) from ep_err
|
||||
|
||||
# Parse response — try multiple known Superset response formats
|
||||
result = response if isinstance(response, dict) else {}
|
||||
|
||||
Reference in New Issue
Block a user