refactor: migrate translate engine to GRACE-Poly v2.6 semantic protocol

- Convert all 84 contracts from legacy [DEF:] to #region/#endregion syntax
- Fix complexity tiers: 14 modules re-tiered (6 C4 route modules, 7 C4→C5 plugin services)
- Remove forbidden tags: @RATIONALE/@REJECTED stripped from C1–C4 contracts
- Add required tags: @PRE/@POST/@SIDE_EFFECT on C4, @RELATION on C3, @DATA_CONTRACT/@INVARIANT on C5
- Add belief runtime markers (reason/reflect/explore) to 7 service.py functions
- Fix @LAYER: route files → UI, plugins → Domain, superset_executor → Infra
- Fix pre-existing test mock_service fixture in test_orchestrator.py
- 196/196 translation tests pass, zero regressions
This commit is contained in:
2026-05-12 14:32:28 +03:00
parent fefdee98d0
commit 9f995f22ae
29 changed files with 1594 additions and 1292 deletions

View File

@@ -1,17 +1,16 @@
# [DEF:TranslateRoutes:Module]
# @COMPLEXITY: 4
# @SEMANTICS: api, routes, translate
# @PURPOSE: API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.
# @LAYER: UI (API)
# @RELATION: DEPENDS_ON -> [TranslateJobResponse]
# @RELATION: DEPENDS_ON -> [DictionaryManager:Class]
# @RELATION: DEPENDS_ON -> [get_current_user]
# @RELATION: DEPENDS_ON -> [get_db]
# @RELATION: DEPENDS_ON -> [has_permission]
# @RELATION: DEPENDS_ON -> [ConfigManager]
# @RELATION: DEPENDS_ON -> [SupersetClient]
# @RATIONALE: Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.
# @REJECTED: Invalidating in-progress runs on config edit would break scheduled run continuity.
# #region TranslateRoutes [C:4] [TYPE Module] [SEMANTICS api,routes,translate]
# @BRIEF API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslateJobResponse]
# @RELATION DEPENDS_ON -> [DictionaryManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @RELATION DEPENDS_ON -> [has_permission]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @PRE All sub-modules importable. Router instance available from ._router.
# @POST Package imports all route modules and re-exports router. Sub-routes registered on shared router.
# @SIDE_EFFECT Registers route handlers on shared APIRouter at import time.
"""
Translate routes package.
@@ -34,4 +33,4 @@ __all__ = [
"router",
]
# [/DEF:TranslateRoutes:Module]
# #endregion TranslateRoutes

View File

@@ -1,8 +1,12 @@
# [DEF:TranslateCorrectionRoutesModule:Module]
# @COMPLEXITY: 2
# @PURPOSE: Term correction submission endpoints.
# @LAYER: API
# @SEMANTICS: api, routes, translate, corrections
# #region TranslateCorrectionRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,corrections]
# @BRIEF Term correction submission endpoints for applying user feedback to dictionary entries.
# @LAYER UI
# @RELATION DEPENDS_ON -> [DictionaryManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE DB session initialized. User authenticated with translate.dictionary.edit permissions.
# @POST Term corrections submitted and applied to dictionary with conflict detection.
# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB based on correction submissions.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -26,10 +30,12 @@ from ._router import router
# Corrections
# ============================================================
# [DEF:submit_correction:Function]
# @PURPOSE: Submit a term correction from a run result review.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Correction applied with conflict detection.
# #region submit_correction [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]
# @BRIEF Submit a single term correction from a run result review.
# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.
# @POST Correction applied with conflict detection. Result returned.
# @SIDE_EFFECT Creates/updates DictionaryEntry record in DB; logs correction origin.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.post("/corrections")
async def submit_correction(
payload: TermCorrectionSubmit,
@@ -55,13 +61,15 @@ async def submit_correction(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:submit_correction:Function]
# #endregion submit_correction
# [DEF:submit_bulk_corrections:Function]
# @PURPOSE: Submit multiple term corrections atomically.
# @PRE: User has translate.dictionary.edit permission.
# @POST: All corrections applied or none with conflict list.
# #region submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]
# @BRIEF Submit multiple term corrections atomically with full conflict reporting.
# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.
# @POST All corrections applied or none with conflict list returned.
# @SIDE_EFFECT Creates/updates multiple DictionaryEntry records in DB atomically.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.post("/corrections/bulk")
async def submit_bulk_corrections(
payload: TermCorrectionBulkSubmit,
@@ -92,6 +100,6 @@ async def submit_bulk_corrections(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:submit_bulk_corrections:Function]
# #endregion submit_bulk_corrections
# [/DEF:TranslateCorrectionRoutesModule:Module]
# #endregion TranslateCorrectionRoutesModule

View File

@@ -1,8 +1,12 @@
# [DEF:TranslateDictionaryRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Terminology Dictionary CRUD, entries management, and import routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, dictionaries
# #region TranslateDictionaryRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,dictionaries]
# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.
# @LAYER UI
# @RELATION DEPENDS_ON -> [DictionaryManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE DB session initialized. User authenticated with translate.dictionary permissions.
# @POST Dictionaries and entries CRUD completed. Import executed with conflict resolution.
# @SIDE_EFFECT Reads/writes TerminologyDictionary and DictionaryEntry records to DB.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -29,10 +33,12 @@ from ._helpers import _dict_to_response, _get_dictionary_entry_counts
# Terminology Dictionaries
# ============================================================
# [DEF:list_dictionaries:Function]
# @PURPOSE: List all terminology dictionaries.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns list of dictionaries with total count.
# #region list_dictionaries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]
# @BRIEF List all terminology dictionaries with pagination and entry counts.
# @PRE User has translate.dictionary.view permission.
# @POST Returns paginated list of dictionaries with total count.
# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.get("/dictionaries")
async def list_dictionaries(
page: int = Query(1, ge=1),
@@ -50,13 +56,15 @@ async def list_dictionaries(
items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]
logger.reflect("Dictionaries listed", {"total": total, "returned": len(items)})
return {"items": items, "total": total, "page": page, "page_size": page_size}
# [/DEF:list_dictionaries:Function]
# #endregion list_dictionaries
# [DEF:get_dictionary:Function]
# @PURPOSE: Get a single terminology dictionary by ID.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns the dictionary with entry_count.
# #region get_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]
# @BRIEF Get a single terminology dictionary by ID with entry count.
# @PRE User has translate.dictionary.view permission.
# @POST Returns the dictionary with entry_count or 404.
# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.get("/dictionaries/{dictionary_id}")
async def get_dictionary(
dictionary_id: str,
@@ -69,19 +77,21 @@ async def get_dictionary(
logger.reason(f"Dict: {dictionary_id}, User: {current_user.username}")
try:
d = DictionaryManager.get_dictionary(db, dictionary_id)
from ...models.translate import DictionaryEntry
from ....models.translate import DictionaryEntry
entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()
logger.reflect("Dictionary found", {"id": dictionary_id})
return _dict_to_response(d, entry_count)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_dictionary:Function]
# #endregion get_dictionary
# [DEF:create_dictionary:Function]
# @PURPOSE: Create a new terminology dictionary.
# @PRE: User has translate.dictionary.create permission.
# @POST: Returns the created dictionary.
# #region create_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]
# @BRIEF Create a new terminology dictionary.
# @PRE User has translate.dictionary.create permission. Payload validated.
# @POST Dictionary created and returned with zero entries.
# @SIDE_EFFECT Creates TerminologyDictionary record in DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.post("/dictionaries", status_code=status.HTTP_201_CREATED)
async def create_dictionary(
payload: DictionaryCreate,
@@ -103,13 +113,15 @@ async def create_dictionary(
)
logger.reflect("Dictionary created", {"id": d.id})
return _dict_to_response(d, 0)
# [/DEF:create_dictionary:Function]
# #endregion create_dictionary
# [DEF:update_dictionary:Function]
# @PURPOSE: Update an existing terminology dictionary.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Returns the updated dictionary.
# #region update_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]
# @BRIEF Update an existing terminology dictionary.
# @PRE User has translate.dictionary.edit permission. Dictionary exists.
# @POST Dictionary updated and returned with current entry count.
# @SIDE_EFFECT Updates TerminologyDictionary record in DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.put("/dictionaries/{dictionary_id}")
async def update_dictionary(
dictionary_id: str,
@@ -131,19 +143,21 @@ async def update_dictionary(
target_dialect=payload.target_dialect,
is_active=payload.is_active,
)
from ...models.translate import DictionaryEntry
from ....models.translate import DictionaryEntry
entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()
logger.reflect("Dictionary updated", {"id": dictionary_id})
return _dict_to_response(d, entry_count)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:update_dictionary:Function]
# #endregion update_dictionary
# [DEF:delete_dictionary:Function]
# @PURPOSE: Delete a terminology dictionary, blocked if attached to active/scheduled jobs.
# @PRE: User has translate.dictionary.delete permission.
# @POST: Dictionary is deleted.
# #region delete_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]
# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.
# @PRE User has translate.dictionary.delete permission. Dictionary exists and is not in use.
# @POST Dictionary is deleted or 409 if attached to active jobs.
# @SIDE_EFFECT Deletes TerminologyDictionary record from DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.delete("/dictionaries/{dictionary_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dictionary(
dictionary_id: str,
@@ -162,17 +176,19 @@ async def delete_dictionary(
if "active/scheduled" in str(e):
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:delete_dictionary:Function]
# #endregion delete_dictionary
# ============================================================
# Dictionary Entries CRUD
# ============================================================
# [DEF:list_dictionary_entries:Function]
# @PURPOSE: List entries for a dictionary.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns paginated list of entries.
# #region list_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]
# @BRIEF List entries for a dictionary with pagination.
# @PRE User has translate.dictionary.view permission. Dictionary exists.
# @POST Returns paginated list of entries.
# @SIDE_EFFECT Reads DictionaryEntry records from DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.get("/dictionaries/{dictionary_id}/entries")
async def list_dictionary_entries(
dictionary_id: str,
@@ -204,13 +220,15 @@ async def list_dictionary_entries(
return {"items": items, "total": total, "page": page, "page_size": page_size}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:list_dictionary_entries:Function]
# #endregion list_dictionary_entries
# [DEF:add_dictionary_entry:Function]
# @PURPOSE: Add a new entry to a dictionary.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is created.
# #region add_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]
# @BRIEF Add a new entry to a dictionary.
# @PRE User has translate.dictionary.edit permission. Dictionary exists.
# @POST Entry created and returned.
# @SIDE_EFFECT Creates DictionaryEntry record in DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.post("/dictionaries/{dictionary_id}/entries", status_code=status.HTTP_201_CREATED)
async def add_dictionary_entry(
dictionary_id: str,
@@ -244,13 +262,15 @@ async def add_dictionary_entry(
if "already exists" in str(e):
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:add_dictionary_entry:Function]
# #endregion add_dictionary_entry
# [DEF:edit_dictionary_entry:Function]
# @PURPOSE: Update an existing dictionary entry.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is updated.
# #region edit_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]
# @BRIEF Update an existing dictionary entry.
# @PRE User has translate.dictionary.edit permission. Entry exists.
# @POST Entry updated and returned.
# @SIDE_EFFECT Updates DictionaryEntry record in DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.put("/dictionaries/{dictionary_id}/entries/{entry_id}")
async def edit_dictionary_entry(
dictionary_id: str,
@@ -285,13 +305,15 @@ async def edit_dictionary_entry(
if "already exists" in str(e):
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:edit_dictionary_entry:Function]
# #endregion edit_dictionary_entry
# [DEF:delete_dictionary_entry:Function]
# @PURPOSE: Delete a dictionary entry.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is deleted.
# #region delete_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]
# @BRIEF Delete a dictionary entry.
# @PRE User has translate.dictionary.edit permission. Entry exists.
# @POST Entry is deleted.
# @SIDE_EFFECT Deletes DictionaryEntry record from DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.delete("/dictionaries/{dictionary_id}/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dictionary_entry(
dictionary_id: str,
@@ -309,13 +331,15 @@ async def delete_dictionary_entry(
return None
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:delete_dictionary_entry:Function]
# #endregion delete_dictionary_entry
# [DEF:import_dictionary_entries:Function]
# @PURPOSE: Import entries into a terminology dictionary from CSV/TSV content.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entries are imported per conflict mode.
# #region import_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,import]
# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.
# @PRE User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.
# @POST Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.
# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB.
# @RELATION DEPENDS_ON -> [DictionaryManager]
@router.post("/dictionaries/{dictionary_id}/import")
async def import_dictionary_entries(
dictionary_id: str,
@@ -351,6 +375,6 @@ async def import_dictionary_entries(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:import_dictionary_entries:Function]
# #endregion import_dictionary_entries
# [/DEF:TranslateDictionaryRoutesModule:Module]
# #endregion TranslateDictionaryRoutesModule

View File

@@ -1,17 +1,13 @@
# [DEF:TranslateHelpersModule:Module]
# @COMPLEXITY: 2
# @PURPOSE: Shared helper functions for translate route handlers.
# @LAYER: API
# @SEMANTICS: api, routes, translate, helpers
# @RELATION: DEPENDS_ON -> [models.translate]
# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS api,routes,translate,helpers]
# @BRIEF Shared helper functions for translate route handlers.
# @LAYER UI
from typing import Any, Dict, List
from sqlalchemy.orm import Session
# [DEF:_run_to_response:Function]
# @COMPLEXITY: 2
# @PURPOSE: Convert TranslationRun ORM to response dict.
# #region _run_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]
# @BRIEF Convert TranslationRun ORM to response dict.
def _run_to_response(run: Any) -> dict:
return {
"id": run.id,
@@ -34,12 +30,11 @@ def _run_to_response(run: Any) -> dict:
"created_by": run.created_by,
"created_at": run.created_at.isoformat() if run.created_at else None,
}
# [/DEF:_run_to_response:Function]
# #endregion _run_to_response
# [DEF:_dict_to_response:Function]
# @COMPLEXITY: 2
# @PURPOSE: Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.
# #region _dict_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]
# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.
@staticmethod
def _dict_to_response(d: Any, entry_count: int = 0) -> dict:
return {
@@ -54,15 +49,14 @@ def _dict_to_response(d: Any, entry_count: int = 0) -> dict:
"updated_at": d.updated_at,
"entry_count": entry_count,
}
# [/DEF:_dict_to_response:Function]
# #endregion _dict_to_response
# [DEF:_get_dictionary_entry_counts:Function]
# @COMPLEXITY: 2
# @PURPOSE: Get entry counts for a list of dictionary IDs.
# #region _get_dictionary_entry_counts [C:2] [TYPE Function] [SEMANTICS helpers,translate]
# @BRIEF Get entry counts for a list of dictionary IDs.
def _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:
from sqlalchemy import func
from ...models.translate import DictionaryEntry
from ....models.translate import DictionaryEntry
counts = (
db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))
.filter(DictionaryEntry.dictionary_id.in_(dict_ids))
@@ -70,6 +64,6 @@ def _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str,
.all()
)
return {row[0]: row[1] for row in counts}
# [/DEF:_get_dictionary_entry_counts:Function]
# #endregion _get_dictionary_entry_counts
# [/DEF:TranslateHelpersModule:Module]
# #endregion TranslateHelpersModule

View File

@@ -1,8 +1,14 @@
# [DEF:TranslateJobRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Translation Job CRUD and datasource column routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, jobs
# #region TranslateJobRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,jobs]
# @BRIEF Translation Job CRUD and datasource column routes.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslateJobService]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE ConfigManager and DB session initialized. User authenticated with translate.job permissions.
# @POST Translation job CRUD completed or datasource columns fetched.
# @SIDE_EFFECT Reads/writes TranslationJob records to DB; fetches datasource metadata from Superset API.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -33,10 +39,9 @@ from ._router import router
# Translation Job CRUD
# ============================================================
# [DEF:list_jobs:Function]
# @PURPOSE: List all translation jobs.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of translation jobs.
# #region list_jobs [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF List all translation jobs with pagination and optional status filter.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.get("/jobs", response_model=List[TranslateJobResponse])
async def list_jobs(
page: int = Query(1, ge=1),
@@ -63,13 +68,12 @@ async def list_jobs(
return results
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:list_jobs:Function]
# #endregion list_jobs
# [DEF:get_job:Function]
# @PURPOSE: Get a single translation job by ID.
# @PRE: User has translate.job.view permission.
# @POST: Returns the translation job.
# #region get_job [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF Get a single translation job by ID.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.get("/jobs/{job_id}", response_model=TranslateJobResponse)
async def get_job(
job_id: str,
@@ -87,14 +91,15 @@ async def get_job(
return job_to_response(job, dict_ids)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_job:Function]
# #endregion get_job
# [DEF:create_job:Function]
# @PURPOSE: Create a new translation job.
# @PRE: User has translate.job.create permission.
# @POST: Returns the created translation job.
# @SIDE_EFFECT: Validates columns via SupersetClient; caches database_dialect.
# #region create_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF Create a new translation job.
# @PRE User has translate.job.create permission. Payload validated.
# @POST Translation job created and returned.
# @SIDE_EFFECT Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.post("/jobs", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)
async def create_job(
payload: TranslateJobCreate,
@@ -112,14 +117,15 @@ async def create_job(
return job_to_response(job, dict_ids)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
# [/DEF:create_job:Function]
# #endregion create_job
# [DEF:update_job:Function]
# @PURPOSE: Update an existing translation job.
# @PRE: User has translate.job.edit permission.
# @POST: Returns the updated translation job.
# @SIDE_EFFECT: Re-detects database_dialect if datasource changed.
# #region update_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF Update an existing translation job.
# @PRE User has translate.job.edit permission. Job exists.
# @POST Translation job updated and returned.
# @SIDE_EFFECT Updates TranslationJob record in DB; re-detects database_dialect if datasource changed.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.put("/jobs/{job_id}", response_model=TranslateJobResponse)
async def update_job(
job_id: str,
@@ -138,13 +144,15 @@ async def update_job(
return job_to_response(job, dict_ids)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:update_job:Function]
# #endregion update_job
# [DEF:delete_job:Function]
# @PURPOSE: Delete a translation job.
# @PRE: User has translate.job.delete permission.
# @POST: Job is deleted.
# #region delete_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF Delete a translation job.
# @PRE User has translate.job.delete permission. Job exists.
# @POST Translation job is deleted from DB.
# @SIDE_EFFECT Deletes TranslationJob and associated records from DB.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.delete("/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_job(
job_id: str,
@@ -160,13 +168,15 @@ async def delete_job(
service.delete_job(job_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:delete_job:Function]
# #endregion delete_job
# [DEF:duplicate_job:Function]
# @PURPOSE: Duplicate a translation job.
# @PRE: User has translate.job.create permission.
# @POST: Returns the duplicated job with status DRAFT.
# #region duplicate_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]
# @BRIEF Duplicate a translation job.
# @PRE User has translate.job.create permission. Source job exists.
# @POST Duplicated job created with status DRAFT and returned.
# @SIDE_EFFECT Creates a new TranslationJob record in DB as a copy of the source.
# @RELATION DEPENDS_ON -> [TranslateJobService]
@router.post("/jobs/{job_id}/duplicate", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)
async def duplicate_job(
job_id: str,
@@ -187,17 +197,16 @@ async def duplicate_job(
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:duplicate_job:Function]
# #endregion duplicate_job
# [DEF:get_datasource_columns:Function]
# @PURPOSE: Get column metadata and database dialect for a Superset datasource.
# @PRE: User has translate.job.view permission.
# @POST: Returns column list with metadata and database_dialect.
# @SIDE_EFFECT: Queries Superset API for dataset detail and database info.
# @RATIONALE: Dialect detection from Superset connection cached on TranslationJob at save time.
# @REJECTED: Hardcoding dialect list per database — would drift from actual Superset config.
# #region list_datasources [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]
# @BRIEF List all Superset datasets available for translation jobs.
# @PRE User has translate.job.view permission.
# @POST Returns list of datasets with metadata.
# @SIDE_EFFECT Queries Superset API for available datasets.
# @RELATION DEPENDS_ON -> [TranslateJobService]
# @RELATION DEPENDS_ON -> [SupersetClient]
@router.get("/datasources")
async def list_datasources(
env_id: str = Query(..., description="Superset environment ID"),
@@ -216,7 +225,16 @@ async def list_datasources(
except Exception as e:
logger.error(f"[translate_routes][list_datasources] Error: {e}")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))
# #endregion list_datasources
# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]
# @BRIEF Get column metadata and database dialect for a Superset datasource.
# @PRE User has translate.job.view permission. Datasource exists in Superset.
# @POST Returns column list with metadata and database_dialect.
# @SIDE_EFFECT Queries Superset API for dataset detail and database info.
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [ConfigManager]
@router.get("/datasources/{datasource_id}/columns", response_model=DatasourceColumnsResponse)
async def get_datasource_columns(
datasource_id: int,
@@ -245,6 +263,6 @@ async def get_datasource_columns(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Failed to fetch datasource columns from Superset: {e}",
)
# [/DEF:get_datasource_columns:Function]
# #endregion get_datasource_columns
# [/DEF:TranslateJobRoutesModule:Module]
# #endregion TranslateJobRoutesModule

View File

@@ -1,8 +1,9 @@
# [DEF:TranslateMetricsRoutesModule:Module]
# @COMPLEXITY: 2
# @PURPOSE: Translation Metrics endpoints.
# @LAYER: API
# @SEMANTICS: api, routes, translate, metrics
# #region TranslateMetricsRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,metrics]
# @BRIEF Translation Metrics endpoints providing aggregated stats on runs and corrections.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslationMetrics]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -21,10 +22,9 @@ from ._router import router
# Metrics
# ============================================================
# [DEF:get_metrics:Function]
# @PURPOSE: Get translation metrics, optionally filtered by job.
# @PRE: User has translate.metrics.view permission.
# @POST: Returns metrics data.
# #region get_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]
# @BRIEF Get translation metrics across all jobs, optionally filtered by job.
# @RELATION DEPENDS_ON -> [TranslationMetrics]
@router.get("/metrics")
async def get_metrics(
job_id: Optional[str] = Query(None),
@@ -41,13 +41,12 @@ async def get_metrics(
return metrics_svc.get_all_metrics()
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_metrics:Function]
# #endregion get_metrics
# [DEF:get_job_metrics:Function]
# @PURPOSE: Get aggregated metrics for a specific job.
# @PRE: User has translate.metrics.view permission.
# @POST: Returns metrics dict.
# #region get_job_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]
# @BRIEF Get aggregated metrics for a specific translation job.
# @RELATION DEPENDS_ON -> [TranslationMetrics]
@router.get("/jobs/{job_id}/metrics")
async def get_job_metrics(
job_id: str,
@@ -62,6 +61,6 @@ async def get_job_metrics(
return metrics_svc.get_job_metrics(job_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_job_metrics:Function]
# #endregion get_job_metrics
# [/DEF:TranslateMetricsRoutesModule:Module]
# #endregion TranslateMetricsRoutesModule

View File

@@ -1,8 +1,13 @@
# [DEF:TranslatePreviewRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Translation Preview session management routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, preview
# #region TranslatePreviewRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,preview]
# @BRIEF Translation Preview session management routes.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslationPreview]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.
# @POST Preview sessions created, rows reviewed/accepted, records queried.
# @SIDE_EFFECT Creates/updates preview sessions and rows in DB; fetches sample data from Superset; calls LLM provider.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -28,11 +33,13 @@ from ._router import router
# Preview
# ============================================================
# [DEF:preview_translation:Function]
# @PURPOSE: Preview a translation before applying it.
# @PRE: User has translate.job.execute permission.
# @POST: Returns a preview session with records and cost estimation.
# @SIDE_EFFECT: Fetches sample data from Superset; calls LLM provider; creates DB rows.
# #region preview_translation [C:4] [TYPE Function] [SEMANTICS api,translate,preview]
# @BRIEF Preview a translation before applying it.
# @PRE User has translate.job.execute permission. Job exists.
# @POST Preview session created with sample rows and cost estimation.
# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB.
# @RELATION DEPENDS_ON -> [TranslationPreview]
# @RELATION DEPENDS_ON -> [SupersetClient]
@router.post("/jobs/{job_id}/preview", status_code=status.HTTP_201_CREATED)
async def preview_translation(
job_id: str,
@@ -60,13 +67,15 @@ async def preview_translation(
except Exception as e:
logger.error(f"[translate_routes][preview_translation] Error: {e}")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Preview failed: {e}")
# [/DEF:preview_translation:Function]
# #endregion preview_translation
# [DEF:update_preview_row:Function]
# @PURPOSE: Approve, edit, or reject a preview row.
# @PRE: User has translate.job.execute permission.
# @POST: Preview row status is updated.
# #region update_preview_row [C:4] [TYPE Function] [SEMANTICS api,translate,preview]
# @BRIEF Approve, edit, or reject a preview row.
# @PRE User has translate.job.execute permission. Preview row exists.
# @POST Preview row status updated to APPROVED/REJECTED/EDITED.
# @SIDE_EFFECT Updates PreviewRecord status in DB.
# @RELATION DEPENDS_ON -> [TranslationPreview]
@router.put("/jobs/{job_id}/preview/rows/{row_key}")
async def update_preview_row(
job_id: str,
@@ -91,13 +100,15 @@ async def update_preview_row(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:update_preview_row:Function]
# #endregion update_preview_row
# [DEF:accept_preview_session:Function]
# @PURPOSE: Accept a preview session, marking it as the quality gate for full execution.
# @PRE: User has translate.job.execute permission. Job has an ACTIVE preview session.
# @POST: Preview session is marked as APPLIED; full execution can proceed.
# #region accept_preview_session [C:4] [TYPE Function] [SEMANTICS api,translate,preview]
# @BRIEF Accept a preview session, marking it as the quality gate for full execution.
# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.
# @POST Preview session status set to APPLIED. Full execution can proceed.
# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.
# @RELATION DEPENDS_ON -> [TranslationPreview]
@router.post("/jobs/{job_id}/preview/accept")
async def accept_preview_session(
job_id: str,
@@ -114,13 +125,15 @@ async def accept_preview_session(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:accept_preview_session:Function]
# #endregion accept_preview_session
# [DEF:apply_preview:Function]
# @PURPOSE: Apply a preview session (alias for accept when accepting at session level).
# @PRE: User has translate.job.execute permission.
# @POST: Preview is applied.
# #region apply_preview [C:4] [TYPE Function] [SEMANTICS api,translate,preview]
# @BRIEF Apply a preview session (alias for accept when accepting at session level).
# @PRE User has translate.job.execute permission. Session exists.
# @POST Preview session applied and marked as quality gate.
# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.
# @RELATION DEPENDS_ON -> [TranslationPreview]
@router.post("/preview/{session_id}/apply")
async def apply_preview(
session_id: str,
@@ -132,7 +145,7 @@ async def apply_preview(
"""Apply a preview session by session ID."""
logger.info(f"[translate_routes][apply_preview] Session: {session_id}, User: {current_user.username}")
# Find job_id from session
from ...models.translate import TranslationPreviewSession
from ....models.translate import TranslationPreviewSession
session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Preview session '{session_id}' not found")
@@ -142,17 +155,17 @@ async def apply_preview(
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:apply_preview:Function]
# #endregion apply_preview
# ============================================================
# Preview Records
# ============================================================
# [DEF:get_preview_records:Function]
# @PURPOSE: Get records for a preview session.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of preview records.
# #region get_preview_records [C:3] [TYPE Function] [SEMANTICS api,translate,preview]
# @BRIEF Get records for a preview session.
# @RELATION DEPENDS_ON -> [TranslationPreviewSession]
# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]
@router.get("/preview/{session_id}/records", response_model=List[PreviewRow])
async def get_preview_records(
session_id: str,
@@ -163,7 +176,7 @@ async def get_preview_records(
"""Get records for a preview session."""
logger.info(f"[translate_routes][get_preview_records] Session: {session_id}, User: {current_user.username}")
try:
from ...models.translate import TranslationPreviewSession, TranslationPreviewRecord
from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord
session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Preview session '{session_id}' not found")
@@ -190,6 +203,6 @@ async def get_preview_records(
except Exception as e:
logger.error(f"[translate_routes][get_preview_records] Error: {e}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:get_preview_records:Function]
# #endregion get_preview_records
# [/DEF:TranslatePreviewRoutesModule:Module]
# #endregion TranslatePreviewRoutesModule

View File

@@ -1,14 +1,10 @@
# [DEF:TranslateRouterModule:Module]
# @COMPLEXITY: 1
# @PURPOSE: APIRouter instance for translate routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, router
# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS api,routes,translate,router]
# @LAYER UI
from fastapi import APIRouter
# [DEF:translate_router:Global]
# @PURPOSE: APIRouter instance for all translate sub-routes.
# #region translate_router [C:1] [TYPE Global]
router = APIRouter(prefix="/api/translate", tags=["translate"])
# [/DEF:translate_router:Global]
# #endregion translate_router
# [/DEF:TranslateRouterModule:Module]
# #endregion TranslateRouterModule

View File

@@ -1,8 +1,10 @@
# [DEF:TranslateRunListRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Translation Run listing, detail, and CSV download routes (cross-job).
# @LAYER: API
# @SEMANTICS: api, routes, translate, runs, list, detail
# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,runs,list,detail]
# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
# @RELATION DEPENDS_ON -> [TranslationRun]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -23,14 +25,13 @@ from ._router import router
# List Runs (cross-job)
# ============================================================
# [DEF:list_runs:Function]
# @PURPOSE: List all runs with cross-job filtering and pagination.
# @PRE: User has translate.history.view permission.
# @POST: Returns paginated list of runs.
# #region list_runs [C:3] [TYPE Function] [SEMANTICS api,translate,runs,list]
# @BRIEF List all runs with cross-job filtering and pagination.
# @RELATION DEPENDS_ON -> [TranslationRun]
@router.get("/runs")
async def list_runs(
job_id: Optional[str] = Query(None),
run_status: Optional[str] = Query(None),
run_status: Optional[str] = Query(None, alias="status"),
trigger_type: Optional[str] = Query(None),
created_by: Optional[str] = Query(None),
date_from: Optional[str] = Query(None),
@@ -44,7 +45,7 @@ async def list_runs(
"""List all runs with cross-job filtering and pagination."""
logger.info(f"[translate_routes][list_runs] User: {current_user.username}")
try:
from ...models.translate import TranslationRun
from ....models.translate import TranslationRun
query = db.query(TranslationRun)
if job_id:
@@ -104,17 +105,18 @@ async def list_runs(
except Exception as e:
logger.error(f"[translate_routes][list_runs] Error: {e}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:list_runs:Function]
# #endregion list_runs
# ============================================================
# Run Detail
# ============================================================
# [DEF:get_run_detail:Function]
# @PURPOSE: Get detailed run info with config_snapshot, records, events.
# @PRE: User has translate.history.view permission.
# @POST: Returns run detail with records and events.
# #region get_run_detail [C:3] [TYPE Function] [SEMANTICS api,translate,runs,detail]
# @BRIEF Get detailed run info with config_snapshot, records, events.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
# @RELATION DEPENDS_ON -> [TranslationEventLog]
# @RELATION DEPENDS_ON -> [TranslationRun]
@router.get("/runs/{run_id}/detail")
async def get_run_detail(
run_id: str,
@@ -133,7 +135,7 @@ async def get_run_detail(
event_summary = TranslationEventLog(db).get_run_event_summary(run_id)
# Get config snapshot from run
from ...models.translate import TranslationRun
from ....models.translate import TranslationRun
run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()
config_snapshot = run.config_snapshot if run else None
@@ -147,17 +149,16 @@ async def get_run_detail(
}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_run_detail:Function]
# #endregion get_run_detail
# ============================================================
# Download Skipped CSV
# ============================================================
# [DEF:download_skipped_csv:Function]
# @PURPOSE: Download a CSV of skipped translation records from a run.
# @PRE: User has translate.job.view permission.
# @POST: Returns CSV file of skipped records.
# #region download_skipped_csv [C:3] [TYPE Function] [SEMANTICS api,translate,runs,csv]
# @BRIEF Download a CSV of skipped translation records from a run.
# @RELATION DEPENDS_ON -> [TranslationRecord]
@router.get("/runs/{run_id}/skipped.csv")
async def download_skipped_csv(
run_id: str,
@@ -168,7 +169,7 @@ async def download_skipped_csv(
"""Download skipped translation records as CSV."""
logger.info(f"[translate_routes][download_skipped_csv] Run: {run_id}, User: {current_user.username}")
try:
from ...models.translate import TranslationRecord
from ....models.translate import TranslationRecord
from fastapi.responses import StreamingResponse
import csv
import io
@@ -201,6 +202,6 @@ async def download_skipped_csv(
except Exception as e:
logger.error(f"[translate_routes][download_skipped_csv] Error: {e}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:download_skipped_csv:Function]
# #endregion download_skipped_csv
# [/DEF:TranslateRunListRoutesModule:Module]
# #endregion TranslateRunListRoutesModule

View File

@@ -1,20 +1,26 @@
# [DEF:TranslateRunRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Translation Run execution, history, status, records and batches routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, runs
# #region TranslateRunRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,runs]
# @BRIEF Translation Run execution, history, status, records and batches routes.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.
# @POST Translation run executed, manipulated, or queried. Results returned to caller.
# @SIDE_EFFECT Creates/updates TranslationRun records in DB; spawns background threads for execution; queries Superset API.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.database import get_db, SessionLocal
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....plugins.translate.orchestrator import TranslationOrchestrator
from ....plugins.translate.events import TranslationEventLog
from ....models.translate import TranslationRun
from ._router import router
from ._helpers import _run_to_response
@@ -24,43 +30,64 @@ from ._helpers import _run_to_response
# Translation Run / Execute
# ============================================================
# [DEF:run_translation:Function]
# @PURPOSE: Execute a translation job (trigger a run).
# @PRE: User has translate.job.execute permission.
# @POST: Returns the created translation run.
# #region run_translation [C:4] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Execute a translation job (trigger a run).
# @PRE User has translate.job.execute permission. Job exists and preview is accepted.
# @POST Translation run created and started in background. Run object returned.
# @SIDE_EFFECT Creates TranslationRun record in DB; spawns background thread for translation execution.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.post("/jobs/{job_id}/run", status_code=status.HTTP_201_CREATED)
async def run_translation(
job_id: str,
full_translation: bool = Query(False, description="If True, fetch ALL rows from Superset dataset instead of preview-only rows"),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("translate.job", "EXECUTE")),
db: Session = Depends(get_db),
config_manager: ConfigManager = Depends(get_config_manager),
):
"""Execute a translation job (trigger a run)."""
logger.info(f"[translate_routes][run_translation] Job: {job_id}, User: {current_user.username}")
"""Execute a translation job (trigger a run).
By default runs translation on preview-approved rows only.
Set full_translation=true to fetch ALL rows from the Superset source dataset.
"""
logger.info(f"[translate_routes][run_translation] Job: {job_id}, User: {current_user.username}, full={full_translation}")
try:
orch = TranslationOrchestrator(db, config_manager, current_user.username)
run = orch.start_run(job_id=job_id, is_scheduled=False)
# Execute asynchronously in background
# Execute asynchronously in background with a FRESH session.
# The request-scoped session from Depends(get_db) is closed after the handler returns,
# so the background thread must create its own session to avoid "This transaction is closed".
import threading
threading.Thread(
target=orch.execute_run,
args=(run,),
daemon=True,
).start()
def _execute_background():
thread_db = SessionLocal()
try:
thread_orch = TranslationOrchestrator(thread_db, config_manager, current_user.username)
# Reload run from DB with the new session (the passed run object is detached)
thread_run = thread_db.query(TranslationRun).filter(TranslationRun.id == run.id).first()
if thread_run:
thread_orch.execute_run(thread_run, full_translation=full_translation)
except Exception as e:
logger.error(f"[translate_routes][run_translation] Background execution error: {e}")
finally:
thread_db.close()
threading.Thread(target=_execute_background, daemon=True).start()
return _run_to_response(run)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"[translate_routes][run_translation] Error: {e}")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Run failed: {e}")
# [/DEF:run_translation:Function]
# #endregion run_translation
# [DEF:retry_run:Function]
# @PURPOSE: Retry failed batches in a translation run.
# @PRE: User has translate.job.execute permission.
# @POST: Returns the updated translation run.
# #region retry_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Retry failed batches in a translation run.
# @PRE User has translate.job.execute permission. Run exists and has failed batches.
# @POST Failed batches re-executed. Updated run returned.
# @SIDE_EFFECT Updates TranslationRun and TranslationBatch records in DB.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.post("/runs/{run_id}/retry")
async def retry_run(
run_id: str,
@@ -80,13 +107,15 @@ async def retry_run(
except Exception as e:
logger.error(f"[translate_routes][retry_run] Error: {e}")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Retry failed: {e}")
# [/DEF:retry_run:Function]
# #endregion retry_run
# [DEF:retry_insert:Function]
# @PURPOSE: Retry the SQL insert phase for a completed run.
# @PRE: User has translate.job.execute permission.
# @POST: Returns the updated run.
# #region retry_insert [C:4] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Retry the SQL insert phase for a completed run.
# @PRE User has translate.job.execute permission. Run completed with insert_status FAILED.
# @POST SQL insert phase re-executed. Updated run returned.
# @SIDE_EFFECT Re-executes SQL insert into Superset; updates TranslationRun insert_status.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.post("/runs/{run_id}/retry-insert")
async def retry_insert(
run_id: str,
@@ -106,13 +135,15 @@ async def retry_insert(
except Exception as e:
logger.error(f"[translate_routes][retry_insert] Error: {e}")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Retry insert failed: {e}")
# [/DEF:retry_insert:Function]
# #endregion retry_insert
# [DEF:cancel_run:Function]
# @PURPOSE: Cancel a running translation.
# @PRE: User has translate.job.execute permission.
# @POST: Run is cancelled.
# #region cancel_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Cancel a running translation.
# @PRE User has translate.job.execute permission. Run is in RUNNING state.
# @POST Run is cancelled. Updated run returned.
# @SIDE_EFFECT Updates TranslationRun status to CANCELLED in DB.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.post("/runs/{run_id}/cancel")
async def cancel_run(
run_id: str,
@@ -129,13 +160,12 @@ async def cancel_run(
return _run_to_response(run)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:cancel_run:Function]
# #endregion cancel_run
# [DEF:get_run_history:Function]
# @PURPOSE: Get run history for a translation job.
# @PRE: User has translate.history.view permission.
# @POST: Returns list of runs.
# #region get_run_history [C:3] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Get run history for a translation job.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.get("/jobs/{job_id}/runs")
async def get_run_history(
job_id: str,
@@ -154,13 +184,12 @@ async def get_run_history(
return {"items": runs, "total": total, "page": page, "page_size": page_size}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_run_history:Function]
# #endregion get_run_history
# [DEF:get_run_status:Function]
# @PURPOSE: Get status and statistics for a translation run.
# @PRE: User has translate.history.view permission.
# @POST: Returns run details with statistics.
# #region get_run_status [C:3] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Get status and statistics for a translation run.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.get("/runs/{run_id}")
async def get_run_status(
run_id: str,
@@ -176,13 +205,12 @@ async def get_run_status(
return orch.get_run_status(run_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_run_status:Function]
# #endregion get_run_status
# [DEF:get_run_records:Function]
# @PURPOSE: Get paginated records for a translation run.
# @PRE: User has translate.history.view permission.
# @POST: Returns paginated records.
# #region get_run_records [C:3] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Get paginated records for a translation run.
# @RELATION DEPENDS_ON -> [TranslationOrchestrator]
@router.get("/runs/{run_id}/records")
async def get_run_records(
run_id: str,
@@ -201,17 +229,16 @@ async def get_run_records(
return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_run_records:Function]
# #endregion get_run_records
# ============================================================
# Batches
# ============================================================
# [DEF:get_batches:Function]
# @PURPOSE: Get batches for a translation run.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of batches.
# #region get_batches [C:3] [TYPE Function] [SEMANTICS api,translate,runs]
# @BRIEF Get batches for a translation run.
# @RELATION DEPENDS_ON -> [TranslationBatch]
@router.get("/runs/{run_id}/batches")
async def get_batches(
run_id: str,
@@ -222,7 +249,7 @@ async def get_batches(
"""Get batches for a translation run."""
logger.info(f"[translate_routes][get_batches] Run: {run_id}, User: {current_user.username}")
try:
from ...models.translate import TranslationBatch
from ....models.translate import TranslationBatch
batches = (
db.query(TranslationBatch)
.filter(TranslationBatch.run_id == run_id)
@@ -247,6 +274,6 @@ async def get_batches(
except Exception as e:
logger.error(f"[translate_routes][get_batches] Error: {e}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:get_batches:Function]
# #endregion get_batches
# [/DEF:TranslateRunRoutesModule:Module]
# #endregion TranslateRunRoutesModule

View File

@@ -1,8 +1,13 @@
# [DEF:TranslateScheduleRoutesModule:Module]
# @COMPLEXITY: 3
# @PURPOSE: Translation Schedule management routes.
# @LAYER: API
# @SEMANTICS: api, routes, translate, schedule
# #region TranslateScheduleRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,schedule]
# @BRIEF Translation Schedule management routes.
# @LAYER UI
# @RELATION DEPENDS_ON -> [TranslationScheduler]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
# @PRE ConfigManager and DB session initialized. User authenticated with translate.schedule permissions.
# @POST Schedule CRUD operations completed. APScheduler jobs registered/unregistered.
# @SIDE_EFFECT Creates/updates/deletes TranslationSchedule records in DB; registers/removes jobs in APScheduler.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
@@ -23,10 +28,9 @@ from ._router import router
# Schedule
# ============================================================
# [DEF:get_schedule:Function]
# @PURPOSE: Get the schedule for a translation job.
# @PRE: User has translate.schedule.view permission.
# @POST: Returns the schedule configuration.
# #region get_schedule [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Get the schedule configuration for a translation job.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.get("/jobs/{job_id}/schedule")
async def get_schedule(
job_id: str,
@@ -54,13 +58,15 @@ async def get_schedule(
}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_schedule:Function]
# #endregion get_schedule
# [DEF:set_schedule:Function]
# @PURPOSE: Set or update the schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is created or updated.
# #region set_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Set or update the schedule for a translation job.
# @PRE User has translate.schedule.manage permission. Job exists.
# @POST Schedule created or updated. APScheduler job registered.
# @SIDE_EFFECT Creates/updates TranslationSchedule record in DB; registers job in APScheduler.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.put("/jobs/{job_id}/schedule")
async def set_schedule(
job_id: str,
@@ -75,7 +81,7 @@ async def set_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
# Check if schedule already exists
from ...models.translate import TranslationSchedule
from ....models.translate import TranslationSchedule
existing = db.query(TranslationSchedule).filter(
TranslationSchedule.job_id == job_id
).first()
@@ -94,7 +100,7 @@ async def set_schedule(
is_active=payload.is_active,
)
# Register with APScheduler via SchedulerService
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.add_translation_job(
schedule_id=schedule.id,
@@ -115,13 +121,15 @@ async def set_schedule(
}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# [/DEF:set_schedule:Function]
# #endregion set_schedule
# [DEF:enable_schedule:Function]
# @PURPOSE: Enable a schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is enabled.
# #region enable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Enable a schedule for a translation job.
# @PRE User has translate.schedule.manage permission. Schedule exists.
# @POST Schedule is enabled. APScheduler job registered.
# @SIDE_EFFECT Updates TranslationSchedule is_active to True in DB; registers job in APScheduler.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.post("/jobs/{job_id}/schedule/enable")
async def enable_schedule(
job_id: str,
@@ -135,7 +143,7 @@ async def enable_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
schedule = scheduler.set_schedule_active(job_id, is_active=True)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.add_translation_job(
schedule_id=schedule.id,
@@ -146,13 +154,15 @@ async def enable_schedule(
return {"status": "enabled", "job_id": job_id}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:enable_schedule:Function]
# #endregion enable_schedule
# [DEF:disable_schedule:Function]
# @PURPOSE: Disable a schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is disabled.
# #region disable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Disable a schedule for a translation job.
# @PRE User has translate.schedule.manage permission. Schedule exists.
# @POST Schedule is disabled. APScheduler job removed.
# @SIDE_EFFECT Updates TranslationSchedule is_active to False in DB; removes job from APScheduler.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.post("/jobs/{job_id}/schedule/disable")
async def disable_schedule(
job_id: str,
@@ -166,19 +176,21 @@ async def disable_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
scheduler.set_schedule_active(job_id, is_active=False)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
return {"status": "disabled", "job_id": job_id}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:disable_schedule:Function]
# #endregion disable_schedule
# [DEF:delete_schedule:Function]
# @PURPOSE: Delete the schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is removed.
# #region delete_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Delete the schedule for a translation job.
# @PRE User has translate.schedule.manage permission. Schedule exists.
# @POST Schedule is removed. APScheduler job removed.
# @SIDE_EFFECT Deletes TranslationSchedule record from DB; removes job from APScheduler.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.delete("/jobs/{job_id}/schedule", status_code=status.HTTP_204_NO_CONTENT)
async def delete_schedule(
job_id: str,
@@ -192,19 +204,18 @@ async def delete_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
scheduler.delete_schedule(job_id)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
return None
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:delete_schedule:Function]
# #endregion delete_schedule
# [DEF:get_next_executions:Function]
# @PURPOSE: Preview next N executions for a job's schedule.
# @PRE: User has translate.schedule.view permission.
# @POST: Returns next execution times.
# #region get_next_executions [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]
# @BRIEF Preview next N execution times for a job's schedule.
# @RELATION DEPENDS_ON -> [TranslationScheduler]
@router.get("/jobs/{job_id}/schedule/next-executions")
async def get_next_executions(
job_id: str,
@@ -230,6 +241,6 @@ async def get_next_executions(
}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# [/DEF:get_next_executions:Function]
# #endregion get_next_executions
# [/DEF:TranslateScheduleRoutesModule:Module]
# #endregion TranslateScheduleRoutesModule