# #region TranslateSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, translate, schema, translate-job-create] # @BRIEF Pydantic v2 schemas for translation API request/response serialization. # @LAYER API # @RELATION DEPENDS_ON -> [EXT:Library:pydantic] from datetime import datetime import re from typing import Any from pydantic import BaseModel, ConfigDict, Field, field_validator def _validate_bcp47_list(v: list[str] | None) -> list[str] | None: """Validate that each item in target_languages is a valid BCP-47 tag.""" if not v: return v for tag in v: if not tag or not tag.strip(): raise ValueError(f"Invalid BCP-47 tag: '{tag}' — must be a non-empty string") if not re.match(r'^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$', tag.strip()): raise ValueError( f"Invalid BCP-47 tag: '{tag}'. " "Expected format like 'en', 'ru', 'zh-CN', 'pt-BR'." ) return v # #region TranslateJobCreate [TYPE Class] # @BRIEF Schema for creating a new translation job. class TranslateJobCreate(BaseModel): name: str description: str | None = None source_dialect: str = Field(..., description="Source database dialect (e.g. postgresql, clickhouse)") target_dialect: str = Field(..., description="Target database dialect (e.g. postgresql, clickhouse)") database_dialect: str | None = Field(None, description="Detected dialect from Superset connection at save time") source_datasource_id: str | None = Field(None, description="Superset datasource ID") source_table: str | None = Field(None, description="Source table name") target_schema: str | None = Field(None, description="Target table schema") target_table: str | None = Field(None, description="Target table name") source_key_cols: list[str] | None = Field(default_factory=list, description="Source key column names") target_key_cols: list[str] | None = Field(default_factory=list, description="Target key column names") translation_column: str | None = Field(None, description="Source column to translate") target_column: str | None = Field(None, description="Target column for translated output (defaults to translation_column)") target_language_column: str | None = Field(None, description="Target column for language code (e.g. 'ru', 'en')") target_source_column: str | None = Field(None, description="Target column for source/original text") target_source_language_column: str | None = Field(None, description="Target column for detected source language (BCP-47)") context_columns: list[str] | None = Field(default_factory=list, description="Context column names") target_language: str | None = Field(None, description="Target language code [DEPRECATED: use target_languages]") target_languages: list[str] | None = Field(default_factory=list, description="List of BCP-47 target language codes") provider_id: str | None = Field(None, description="LLM provider ID") _validate_target_languages = field_validator("target_languages")(_validate_bcp47_list) batch_size: int = Field(50, description="Records per batch") disable_reasoning: bool = Field(False, description="If true, pass reasoning_effort:none to LLM to save output tokens") upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE") dictionary_ids: list[str] | None = Field(default_factory=list, description="Associated terminology dictionary IDs") environment_id: str | None = Field(None, description="Superset environment ID") target_database_id: str | None = Field(None, description="Superset database ID for SQL Lab insert target") # #endregion TranslateJobCreate # #region TranslateJobUpdate [TYPE Class] # @BRIEF Schema for updating an existing translation job. class TranslateJobUpdate(BaseModel): name: str | None = None description: str | None = None source_dialect: str | None = None target_dialect: str | None = None database_dialect: str | None = None source_datasource_id: str | None = None source_table: str | None = None target_schema: str | None = None target_table: str | None = None source_key_cols: list[str] | None = None target_key_cols: list[str] | None = None translation_column: str | None = None target_column: str | None = None target_language_column: str | None = None target_source_column: str | None = None target_source_language_column: str | None = None context_columns: list[str] | None = None target_language: str | None = None target_languages: list[str] | None = None provider_id: str | None = None _validate_target_languages = field_validator("target_languages")(_validate_bcp47_list) batch_size: int | None = None disable_reasoning: bool | None = None upsert_strategy: str | None = None status: str | None = None dictionary_ids: list[str] | None = None environment_id: str | None = None target_database_id: str | None = None # #endregion TranslateJobUpdate # #region TranslateJobResponse [TYPE Class] # @BRIEF Schema for translation job API responses. class TranslateJobResponse(BaseModel): id: str name: str description: str | None = None source_dialect: str target_dialect: str database_dialect: str | None = None source_datasource_id: str | None = None source_table: str | None = None target_schema: str | None = None target_table: str | None = None source_key_cols: list[str] | None = None target_key_cols: list[str] | None = None translation_column: str | None = None target_column: str | None = None target_language_column: str | None = None target_source_column: str | None = None target_source_language_column: str | None = None context_columns: list[str] | None = None # source_language removed — deprecated, per-row auto-detected target_languages: list[str] | None = None provider_id: str | None = None batch_size: int = 50 disable_reasoning: bool = False upsert_strategy: str = "MERGE" status: str created_by: str | None = None created_at: datetime updated_at: datetime | None = None dictionary_ids: list[str] | None = None environment_id: str | None = None target_database_id: str | None = None model_config = ConfigDict(from_attributes=True) # #endregion TranslateJobResponse # #region DatasourceColumnResponse [TYPE Class] # @BRIEF Schema for datasource column metadata response. class DatasourceColumnResponse(BaseModel): name: str type: str | None = None is_physical: bool = True is_dttm: bool = False description: str | None = None # #endregion DatasourceColumnResponse # #region DatasourceColumnsResponse [TYPE Class] # @BRIEF Schema for datasource columns endpoint response. class DatasourceColumnsResponse(BaseModel): datasource_id: int datasource_name: str | None = None schema_name: str | None = None database_dialect: str columns: list[DatasourceColumnResponse] = [] model_config = ConfigDict(from_attributes=True) # #endregion DatasourceColumnsResponse # #region DuplicateJobResponse [TYPE Class] # @BRIEF Schema for duplicate job response. class DuplicateJobResponse(BaseModel): id: str name: str message: str = "Job duplicated successfully" model_config = ConfigDict(from_attributes=True) # #endregion DuplicateJobResponse # #region DictionaryCreate [TYPE Class] # @BRIEF Schema for creating a new terminology dictionary. class DictionaryCreate(BaseModel): name: str description: str | None = None is_active: bool = True # #endregion DictionaryCreate # #region DictionaryImport [TYPE Class] # @BRIEF Schema for importing entries into a terminology dictionary. class DictionaryImport(BaseModel): content: str = Field(..., description="CSV or TSV content as raw string") delimiter: str | None = Field(None, description="Detected or forced delimiter: ',' or '\\t'. Auto-detect if omitted.") on_conflict: str = Field("overwrite", description="'overwrite' or 'keep_existing' or 'cancel'") preview_only: bool = Field(False, description="If true, return preview without applying") default_source_language: str | None = Field(None, description="Default BCP-47 source language when column missing in data") default_target_language: str | None = Field(None, description="Default BCP-47 target language when column missing in data") # #endregion DictionaryImport # #region DictionaryResponse [TYPE Class] # @BRIEF Schema for terminology dictionary API responses. class DictionaryResponse(BaseModel): id: str name: str description: str | None = None is_active: bool created_by: str | None = None created_at: datetime updated_at: datetime | None = None entry_count: int | None = None model_config = ConfigDict(from_attributes=True) # #endregion DictionaryResponse # #region DictionaryEntryCreate [TYPE Class] # @BRIEF Schema for adding/editing a dictionary entry with language pair support. class DictionaryEntryCreate(BaseModel): source_term: str = Field(..., description="Source term to translate") target_term: str = Field(..., description="Target/translated term") context_notes: str | None = Field(None, description="Optional context notes") source_language: str = Field("und", description="BCP-47 source language code (default: und)") target_language: str = Field("und", description="BCP-47 target language code (default: und)") model_config = ConfigDict(from_attributes=True) # #endregion DictionaryEntryCreate # #region DictionaryEntryResponse [TYPE Class] # @BRIEF Schema for dictionary entry API responses. class DictionaryEntryResponse(BaseModel): id: str dictionary_id: str source_term: str source_term_normalized: str target_term: str source_language: str | None = None target_language: str | None = None context_notes: str | None = None context_data: dict[str, Any] | None = None usage_notes: str | None = None has_context: bool = False context_source: str | None = None origin_source_language: str | None = None created_at: datetime updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) # #endregion DictionaryEntryResponse # #region DictionaryImportResult [TYPE Class] # @BRIEF Schema for dictionary import result. class DictionaryImportResult(BaseModel): total: int = 0 created: int = 0 updated: int = 0 skipped: int = 0 errors: list[dict[str, Any]] = Field(default_factory=list) preview: list[dict[str, Any]] = Field(default_factory=list, description="Preview rows with conflict flags") # #endregion DictionaryImportResult # #region PreviewRequest [TYPE Class] # @BRIEF Schema for triggering a translation preview. class PreviewRequest(BaseModel): sample_size: int = Field(10, ge=1, le=100, description="Number of sample rows to preview") prompt_template: str | None = Field(None, description="Optional custom prompt template") env_id: str | None = Field(None, description="Superset environment ID for preview data fetch") # #endregion PreviewRequest # #region PreviewRowUpdate [TYPE Class] # @BRIEF Schema for approving/editing/rejecting a preview row. class PreviewRowUpdate(BaseModel): action: str = Field(..., description="'approve', 'reject', or 'edit'") translation: str | None = Field(None, description="Edited translation (required for 'edit' action)") feedback: str | None = Field(None, description="Optional feedback/comment") language_code: str | None = Field(None, description="BCP-47 language code for per-language actions (optional, applies to all if omitted)") # #endregion PreviewRowUpdate # #region PreviewAcceptResponse [TYPE Class] # @BRIEF Schema for preview accept response. class PreviewAcceptResponse(BaseModel): id: str job_id: str status: str created_by: str | None = None created_at: datetime expires_at: datetime | None = None records: list['PreviewRow'] = [] target_languages: list[str] = Field(default_factory=list, description="Target languages for this preview") model_config = ConfigDict(from_attributes=True) # #endregion PreviewAcceptResponse # #region CostEstimate [TYPE Class] # @BRIEF Schema for cost estimation in preview response. class CostEstimate(BaseModel): sample_size: int = 0 num_languages: int = 1 sample_prompt_tokens: int = 0 sample_output_tokens: int = 0 sample_total_tokens: int = 0 sample_cost: float = 0.0 estimated_total_rows: int = 0 estimated_tokens: int = 0 estimated_cost: float = 0.0 warning: str | None = None # #endregion CostEstimate # #region TermCorrectionSubmit [TYPE Class] # @BRIEF Schema for submitting a term correction in a translation preview. class TermCorrectionSubmit(BaseModel): source_term: str incorrect_target_term: str corrected_target_term: str dictionary_id: str | None = Field(None, description="Target dictionary ID (language-filtered)") origin_run_id: str | None = Field(None, description="Run ID from which this correction originated") origin_row_key: str | None = Field(None, description="Row key within the run") context_data: dict[str, Any] | None = Field(None, description="Context data for the dictionary entry") usage_notes: str | None = Field(None, description="Usage notes for the dictionary entry") keep_context: bool = Field(True, description="If false, clear context_data and set has_context=False on dictionary entry") # #endregion TermCorrectionSubmit # #region TermCorrectionBulkSubmit [TYPE Class] # @BRIEF Schema for submitting multiple term corrections atomically. class TermCorrectionBulkSubmit(BaseModel): corrections: list[TermCorrectionSubmit] dictionary_id: str = Field(..., description="Target dictionary ID for all corrections") # #endregion TermCorrectionBulkSubmit # #region CorrectionConflictResult [TYPE Class] # @BRIEF Schema for reporting conflicts in correction submission. class CorrectionConflictResult(BaseModel): source_term: str existing_target_term: str submitted_target_term: str action: str = "keep_existing" # #endregion CorrectionConflictResult # #region CorrectionSubmitResponse [TYPE Class] # @BRIEF Schema for correction submission response. class CorrectionSubmitResponse(BaseModel): entry_id: str | None = None action: str # "created", "updated", "conflict_detected", "skipped" source_term: str target_term: str conflict: CorrectionConflictResult | None = None message: str | None = None # #endregion CorrectionSubmitResponse # #region ScheduleConfig [TYPE Class] # @BRIEF Schema for configuring a recurring translation schedule. class ScheduleConfig(BaseModel): cron_expression: str = Field(..., description="Cron expression for scheduling (e.g. '0 2 * * *')") timezone: str = Field("UTC", description="Timezone for the cron schedule (e.g. 'UTC', 'Europe/Moscow')") is_active: bool = True execution_mode: str = Field("full", description="Translation execution mode: 'full' or 'new_key_only'") # #endregion ScheduleConfig # #region ScheduleResponse [TYPE Class] # @BRIEF Schema for schedule API responses. class ScheduleResponse(BaseModel): id: str job_id: str cron_expression: str timezone: str is_active: bool execution_mode: str = "full" last_run_at: datetime | None = None next_run_at: datetime | None = None created_by: str | None = None created_at: datetime updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) # #endregion ScheduleResponse # #region NextExecutionResponse [TYPE Class] # @BRIEF Schema for next execution preview. class NextExecutionResponse(BaseModel): job_id: str cron_expression: str timezone: str next_executions: list[str] = [] # #endregion NextExecutionResponse # #region TranslationRunResponse [TYPE Class] # @BRIEF Schema for translation run API responses. class TranslationRunResponse(BaseModel): id: str job_id: str status: str trigger_type: str | None = None started_at: datetime | None = None completed_at: datetime | None = None error_message: str | None = None total_records: int = 0 successful_records: int = 0 failed_records: int = 0 skipped_records: int = 0 cache_hits: int = 0 insert_status: str | None = None superset_execution_id: str | None = None config_snapshot: dict[str, Any] | None = None key_hash: str | None = None config_hash: str | None = None dict_snapshot_hash: str | None = None created_by: str | None = None created_at: datetime language_stats: list['TranslationRunLanguageStatsResponse'] | None = None model_config = ConfigDict(from_attributes=True) # #endregion TranslationRunResponse # #region RunDetailResponse [TYPE Class] # @BRIEF Schema for detailed run response including records, events, and config snapshot. class RunDetailResponse(BaseModel): run: TranslationRunResponse records: list[dict[str, Any]] = Field(default_factory=list, description="Paginated translation records") events: list[dict[str, Any]] = Field(default_factory=list, description="Run events") event_invariants: dict[str, Any] | None = None batch_count: int = 0 # #endregion RunDetailResponse # #region RunHistoryFilter [TYPE Class] # @BRIEF Schema for filtering run history. class RunHistoryFilter(BaseModel): job_id: str | None = None status: str | None = None trigger_type: str | None = None created_by: str | None = None date_from: datetime | None = None date_to: datetime | None = None page: int = 1 page_size: int = 20 # #endregion RunHistoryFilter # #region RunListResponse [TYPE Class] # @BRIEF Schema for paginated run list response. class RunListResponse(BaseModel): items: list[TranslationRunResponse] = [] total: int = 0 page: int = 1 page_size: int = 20 # #endregion RunListResponse # #region AggregatedMetricsResponse [TYPE Class] # @BRIEF Schema for aggregated per-job metrics. class AggregatedMetricsResponse(BaseModel): job_id: str total_runs: int = 0 successful_runs: int = 0 failed_runs: int = 0 cancelled_runs: int = 0 total_records: int = 0 successful_records: int = 0 failed_records: int = 0 skipped_records: int = 0 cumulative_tokens: int | None = None cumulative_cost: float | None = None avg_duration_ms: float | None = None last_run_at: datetime | None = None next_scheduled_run: datetime | None = None # #endregion AggregatedMetricsResponse # #region TranslationPreviewLanguageResponse [C:1] [TYPE Class] # @BRIEF Schema for per-language preview data in API responses. class TranslationPreviewLanguageResponse(BaseModel): language_code: str source_language_detected: str | None = None translated_value: str | None = None user_edit: str | None = None final_value: str | None = None status: str = "pending" needs_review: bool = False model_config = ConfigDict(from_attributes=True) # #endregion TranslationPreviewLanguageResponse # #region PreviewRow [TYPE Class] # @BRIEF A single row in a translation preview showing original and translated content side by side. class PreviewRow(BaseModel): id: str source_sql: str | None = None target_sql: str | None = None source_object_type: str | None = None source_object_id: str | None = None source_object_name: str | None = None status: str = "PENDING" feedback: str | None = None source_language_detected: str | None = None needs_review: bool = False languages: list[TranslationPreviewLanguageResponse] = Field(default_factory=list, description="Per-language translation data") # #endregion PreviewRow # #region TranslationPreviewResponse [TYPE Class] # @BRIEF Schema for translation preview session API responses. class TranslationPreviewResponse(BaseModel): id: str job_id: str run_id: str | None = None status: str created_by: str | None = None created_at: datetime expires_at: datetime | None = None records: list[PreviewRow] = [] target_languages: list[str] = Field(default_factory=list, description="Target languages for this preview") cost_estimate: CostEstimate | None = None model_config = ConfigDict(from_attributes=True) # #endregion TranslationPreviewResponse # #region TranslationBatchResponse [TYPE Class] # @BRIEF Schema for translation batch API responses. class TranslationBatchResponse(BaseModel): id: str run_id: str batch_index: int status: str total_records: int = 0 successful_records: int = 0 failed_records: int = 0 started_at: datetime | None = None completed_at: datetime | None = None created_at: datetime model_config = ConfigDict(from_attributes=True) # #endregion TranslationBatchResponse # #region MetricsResponse [TYPE Class] # @BRIEF Schema for translation metrics API responses. class MetricsResponse(BaseModel): job_id: str snapshot_date: datetime total_jobs: int = 0 total_runs: int = 0 total_records: int = 0 successful_records: int = 0 failed_records: int = 0 skipped_records: int = 0 avg_duration_ms: int | None = None p50_duration_ms: int | None = None p95_duration_ms: int | None = None p99_duration_ms: int | None = None model_config = ConfigDict(from_attributes=True) # #endregion MetricsResponse # #region TranslationLanguageResponse [C:1] [TYPE Class] # @BRIEF Schema for per-language translation data in API responses. class TranslationLanguageResponse(BaseModel): language_code: str source_language_detected: str | None = None translated_value: str | None = None user_edit: str | None = None final_value: str | None = None status: str = "pending" needs_review: bool = False language_overridden: bool = False model_config = ConfigDict(from_attributes=True) # #endregion TranslationLanguageResponse # #region TranslationRunLanguageStatsResponse [C:1] [TYPE Class] # @BRIEF Schema for per-language statistics in run API responses. class TranslationRunLanguageStatsResponse(BaseModel): language_code: str total_rows: int = 0 translated_rows: int = 0 failed_rows: int = 0 skipped_rows: int = 0 token_count: int = 0 estimated_cost: float = 0.0 model_config = ConfigDict(from_attributes=True) # #endregion TranslationRunLanguageStatsResponse # #region OverrideLanguageRequest [C:1] [TYPE Class] # @BRIEF Schema for manually overriding the detected source language of a translation. class OverrideLanguageRequest(BaseModel): source_language: str = Field(..., description="BCP-47 language code to override with") # #endregion OverrideLanguageRequest # #region BulkFindReplaceRequest [C:1] [TYPE Class] # @BRIEF Schema for bulk find-and-replace within translations for a target language. class BulkFindReplaceRequest(BaseModel): find_pattern: str = Field(..., description="Text or regex pattern to find", max_length=500) is_regex: bool = Field(False, description="Whether find_pattern is a regular expression") replacement_text: str = Field(..., description="Replacement text") target_language: str = Field(..., description="BCP-47 language code to target") preview: bool = Field(True, description="If true, return matches without applying replacements") submit_to_dictionary: bool = Field(False, description="If true, also submit corrections to dictionary") dictionary_id: str | None = Field(None, description="Target dictionary ID for submitting corrections") usage_notes: str | None = Field(None, description="Usage notes for dictionary entries") submit_to_dictionary_with_context: bool = Field(False, description="If true, auto-capture source row context when submitting to dictionary") @field_validator("find_pattern") @classmethod def validate_pattern_length(cls, v: str) -> str: """Reject patterns longer than 500 characters to prevent ReDoS.""" if len(v) > 500: raise ValueError("Pattern too long (max 500 characters)") return v # #endregion BulkFindReplaceRequest # #region InlineEditRequest [C:1] [TYPE Class] # @BRIEF Schema for inline editing a translated value on a completed run result. class InlineEditRequest(BaseModel): final_value: str = Field(..., description="Corrected/edited translation text") submit_to_dictionary: bool = Field(False, description="If true, also save correction to dictionary") dictionary_id: str | None = Field(None, description="Target dictionary ID for saving correction") context_data_override: dict[str, Any] | None = Field(None, description="Override auto-captured context data for dictionary entry") usage_notes: str | None = Field(None, description="Usage notes for the dictionary entry") keep_context: bool = Field(True, description="If false, clear context_data and set has_context=False on dictionary entry") # #endregion InlineEditRequest # #region BulkReplacePreviewItem [C:1] [TYPE Class] # @BRIEF A single item in a bulk replace preview list. class BulkReplacePreviewItem(BaseModel): record_id: str language_code: str source_term: str current_value: str new_value: str source_language_detected: str | None = None # #endregion BulkReplacePreviewItem # #region BulkReplaceResponse [C:1] [TYPE Class] # @BRIEF Response for bulk find-and-replace operation. class BulkReplaceResponse(BaseModel): rows_affected: int = 0 corrections_submitted: int = 0 preview: list[BulkReplacePreviewItem] = Field(default_factory=list) # #endregion BulkReplaceResponse # #region InlineCorrectionSubmit [C:1] [TYPE Class] # @BRIEF Schema for submitting an inline correction for a specific translated record. class InlineCorrectionSubmit(BaseModel): record_id: str = Field(..., description="Translation record ID") language_code: str = Field(..., description="BCP-47 language code of the translated value") source_term: str = Field(..., description="Original source term that was incorrectly translated") corrected_term: str = Field(..., description="Corrected translation") dictionary_id: str = Field(..., description="Target dictionary ID to also save the correction") source_language: str = Field(..., description="BCP-47 source language code") # #endregion InlineCorrectionSubmit # #region TargetSchemaValidationRequest [C:1] [TYPE Class] # @BRIEF Schema for requesting target table schema validation. class TargetSchemaValidationRequest(BaseModel): environment_id: str = Field(..., description="Superset environment ID") target_database_id: str = Field(..., description="Superset database ID for the target DB") target_schema: str = Field("public", description="Target table schema (default: public)") target_table: str = Field(..., description="Target table name") # Column mapping from job config (everything that affects expected columns) target_key_cols: list[str] | None = Field(default_factory=list, description="Target key column names") target_column: str | None = Field(None, description="Target column for translated output") translation_column: str | None = Field(None, description="Source column name (fallback for target_column)") target_language_column: str | None = Field(None, description="Column for language code") target_source_column: str | None = Field(None, description="Column for source text") target_source_language_column: str | None = Field(None, description="Column for detected source language") # #endregion TargetSchemaValidationRequest # #region TargetSchemaColumnInfo [C:1] [TYPE Class] # @BRIEF Schema for a single column in the schema validation response. class TargetSchemaColumnInfo(BaseModel): name: str data_type: str | None = None is_nullable: bool = True # #endregion TargetSchemaColumnInfo # #region TargetSchemaValidationResponse [C:1] [TYPE Class] # @BRIEF Schema for target table schema validation response. class TargetSchemaValidationResponse(BaseModel): table_exists: bool = False error: str | None = None expected_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description="Columns that the INSERT expects") actual_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description="Columns that exist in the target table") missing_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description="Expected columns NOT found in target table") extra_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description="Actual columns NOT in expected list") all_present: bool = False database_name: str | None = Field(default=None, description="Actual database name queried (from Superset)") database_backend: str | None = Field(default=None, description="Actual database backend dialect (from Superset)") # #endregion TargetSchemaValidationResponse # #endregion TranslateSchemas