CRITICAL (CVE-class): - C-4: Remove DEV_MODE fallback with hardcoded postgres:postgres credentials - C-3: WebSocket endpoints now require JWT or API key token (?token=) auth - C-1: Superset environment passwords encrypted via Fernet at rest in DB - H-4: SESSION_SECRET_KEY separated from JWT AUTH_SECRET_KEY - H-1: Log injection via %s-formatting instead of f-strings - H-5: X-Trace-ID header validated as UUID4 to prevent trace poisoning INFRASTRUCTURE: - New src/core/encryption.py — EncryptionManager extracted from llm_provider - Test DB: per-module sqlite:///:memory: with PRAGMA foreign_keys=ON - testcontainers PostgreSQL via TEST_DB=postgres env var - conftest temp-file global engine (no 10GB shared-cache leak) TEST FIXES (44 pre-existing → 0): - test_smoke_plugins: module-level sys.modules mock isolated to per-test fixture - test_migration_engine: EXT:Python:uuid → uuid syntax fix - test_dashboards_api: mock env attributes + correct patch target - test_constants_audit_fixes: sync expected constant names with actual - test_defensive_guards: patch.object instead of module-level Repo mock - test_clean_release_cli: removed empty config.json directory - FK violations: TaskRecord parents in log_persistence, Environment in mapping_service, ReleaseCandidate in candidate_manifest_services ORTHOGONAL TESTS (18 new): - test_security_orthogonal.py: bcrypt 72-byte limit, unicode, API key format invariants, Fernet robustness, encryption key lifecycle, log injection protection, JWT edge cases MODEL FIXES: - MetricSnapshot.job_id: nullable with SET NULL (was broken FK for aggregate prune snapshots, hidden by SQLite) CLEANUP: - Removed stale :memory:test_main/test_auth/test_tasks file databases - Removed duplicate #endregion in encryption_key.py
401 lines
22 KiB
Python
401 lines
22 KiB
Python
# #region TranslateModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, translate, model, schema, dashboard, llm]
|
|
# @BRIEF SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects.
|
|
# @LAYER Domain
|
|
# @RELATION INHERITS -> [MappingModels]
|
|
|
|
from datetime import UTC, datetime
|
|
import uuid
|
|
|
|
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from .mapping import Base
|
|
|
|
|
|
def generate_uuid():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
# #region TranslationJob [TYPE Class]
|
|
# @BRIEF A translation job representing a multi-dialect conversion task with column mappings, LLM config, and dictionary attachments.
|
|
# @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.
|
|
class TranslationJob(Base):
|
|
__tablename__ = "translation_jobs"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
name = Column(String, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
source_dialect = Column(String, nullable=False)
|
|
target_dialect = Column(String, nullable=False)
|
|
database_dialect = Column(String, nullable=True, comment="Detected dialect from Superset connection at save time")
|
|
status = Column(String, nullable=False, default="DRAFT") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED
|
|
|
|
# Datasource & target table configuration
|
|
source_datasource_id = Column(String, nullable=True, comment="Superset datasource ID")
|
|
source_table = Column(String, nullable=True, comment="Source table name resolved from datasource")
|
|
target_schema = Column(String, nullable=True, comment="Target table schema")
|
|
target_table = Column(String, nullable=True, comment="Target table name")
|
|
|
|
# Column mapping
|
|
source_key_cols = Column(JSON, nullable=True, comment="Source key column names for composite key")
|
|
target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key")
|
|
translation_column = Column(String, nullable=True, comment="Source column whose values will be translated")
|
|
target_column = Column(String, nullable=True, comment="Target column for translated output (defaults to translation_column)")
|
|
target_language_column = Column(String, nullable=True, comment="Target column for language code (e.g. 'ru', 'en')")
|
|
target_source_column = Column(String, nullable=True, comment="Target column for source/original text")
|
|
target_source_language_column = Column(String, nullable=True, comment="Target column for detected source language (BCP-47)")
|
|
context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt")
|
|
|
|
# LLM & processing settings
|
|
# source_language removed — deprecated, auto-detected per row by LLM; use TranslationLanguage.source_language_detected instead
|
|
target_languages = Column(JSON, nullable=True, comment="List of BCP-47 target language codes (multi-language support)")
|
|
provider_id = Column(String, nullable=True, comment="LLM provider ID")
|
|
batch_size = Column(Integer, nullable=False, default=50, comment="Records per batch")
|
|
disable_reasoning = Column(Boolean, default=False, comment="If true, pass reasoning_effort:none to LLM to save output tokens")
|
|
upsert_strategy = Column(String, nullable=False, default="MERGE", comment="MERGE, INSERT, UPDATE")
|
|
|
|
# Environment association
|
|
environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access")
|
|
target_database_id = Column(String, nullable=True, comment="Superset database ID for SQL Lab insert target")
|
|
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
|
# #endregion TranslationJob
|
|
|
|
|
|
# #region TranslationRun [TYPE Class]
|
|
# @BRIEF Represents a single execution of a translation job, capturing status, timing, and Superset execution metadata.
|
|
class TranslationRun(Base):
|
|
__tablename__ = "translation_runs"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
status = Column(String, nullable=False, default="PENDING") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED
|
|
trigger_type = Column(String, nullable=True, comment="manual, scheduled, retry, baseline_expired")
|
|
started_at = Column(DateTime, nullable=True)
|
|
completed_at = Column(DateTime, nullable=True)
|
|
error_message = Column(Text, nullable=True)
|
|
total_records = Column(Integer, default=0)
|
|
successful_records = Column(Integer, default=0)
|
|
failed_records = Column(Integer, default=0)
|
|
skipped_records = Column(Integer, default=0)
|
|
insert_status = Column(String, nullable=True, comment="Status of the Superset insert/update operation")
|
|
superset_execution_id = Column(String, nullable=True, comment="Superset execution/task ID")
|
|
superset_execution_log = Column(JSON, nullable=True, comment="Superset execution log output")
|
|
config_snapshot = Column(JSON, nullable=True, comment="Snapshot of job config at run creation time")
|
|
key_hash = Column(String, nullable=True, comment="Hash of source key fields for dedup")
|
|
config_hash = Column(String, nullable=True, comment="Hash of translation configuration state")
|
|
dict_snapshot_hash = Column(String, nullable=True, comment="Hash of dictionary state at run time")
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
|
|
language_stats = relationship("TranslationRunLanguageStats", back_populates="run")
|
|
# #endregion TranslationRun
|
|
|
|
|
|
# #region TranslationBatch [TYPE Class]
|
|
# @BRIEF Groups translation records within a run into manageable batches with timing and record counts.
|
|
class TranslationBatch(Base):
|
|
__tablename__ = "translation_batches"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
batch_index = Column(Integer, nullable=False)
|
|
status = Column(String, nullable=False, default="PENDING")
|
|
total_records = Column(Integer, default=0)
|
|
successful_records = Column(Integer, default=0)
|
|
failed_records = Column(Integer, default=0)
|
|
started_at = Column(DateTime, nullable=True)
|
|
completed_at = Column(DateTime, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
# #endregion TranslationBatch
|
|
|
|
|
|
# #region TranslationRecord [TYPE Class]
|
|
# @BRIEF Individual translation result for a single SQL statement or dashboard element, tracking source, target, and error state.
|
|
class TranslationRecord(Base):
|
|
__tablename__ = "translation_records"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
batch_id = Column(String, ForeignKey("translation_batches.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
source_sql = Column(Text, nullable=True)
|
|
target_sql = Column(Text, nullable=True)
|
|
source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset
|
|
source_object_id = Column(String, nullable=True)
|
|
source_object_name = Column(String, nullable=True)
|
|
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
|
|
status = Column(String, nullable=False, default="PENDING") # PENDING, SUCCESS, FAILED, SKIPPED
|
|
error_message = Column(Text, nullable=True)
|
|
token_count_input = Column(Integer, nullable=True)
|
|
token_count_output = Column(Integer, nullable=True)
|
|
translation_duration_ms = Column(Integer, nullable=True)
|
|
source_hash = Column(String, nullable=True,
|
|
comment="SHA256(source_text+context+dict_snapshot_hash+config_hash) for cache dedup — excludes key/identifier columns")
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
|
|
languages = relationship("TranslationLanguage", back_populates="record")
|
|
|
|
__table_args__ = (
|
|
Index("ix_translation_records_run_status", "run_id", "status"),
|
|
Index("ix_translation_records_source_hash_status", "source_hash", "status"),
|
|
)
|
|
# #endregion TranslationRecord
|
|
|
|
|
|
# #region TranslationEvent [TYPE Class]
|
|
# @BRIEF Audit/event log for translation operations, with optional run_id for context.
|
|
class TranslationEvent(Base):
|
|
__tablename__ = "translation_events"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.
|
|
event_data = Column(JSON, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
# #endregion TranslationEvent
|
|
|
|
|
|
# #region TranslationPreviewSession [TYPE Class]
|
|
# @BRIEF A preview session allowing users to review proposed translations before applying them.
|
|
class TranslationPreviewSession(Base):
|
|
__tablename__ = "translation_preview_sessions"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True)
|
|
status = Column(String, nullable=False, default="ACTIVE") # ACTIVE, APPLIED, DISCARDED, EXPIRED
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
expires_at = Column(DateTime, nullable=True)
|
|
# #endregion TranslationPreviewSession
|
|
|
|
|
|
# #region TranslationPreviewRecord [TYPE Class]
|
|
# @BRIEF Individual preview entry within a preview session, showing original and translated content side by side.
|
|
class TranslationPreviewRecord(Base):
|
|
__tablename__ = "translation_preview_records"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
session_id = Column(String, ForeignKey("translation_preview_sessions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
source_sql = Column(Text, nullable=True)
|
|
target_sql = Column(Text, nullable=True)
|
|
source_object_type = Column(String, nullable=True)
|
|
source_object_id = Column(String, nullable=True)
|
|
source_object_name = Column(String, nullable=True)
|
|
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
|
|
status = Column(String, nullable=False, default="PENDING") # PENDING, APPROVED, REJECTED
|
|
feedback = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
|
|
languages = relationship("TranslationPreviewLanguage", back_populates="preview_record")
|
|
# #endregion TranslationPreviewRecord
|
|
|
|
|
|
# #region TerminologyDictionary [TYPE Class]
|
|
# @BRIEF A named collection of terminology mappings used during translation to ensure consistent term translation.
|
|
class TerminologyDictionary(Base):
|
|
__tablename__ = "terminology_dictionaries"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
name = Column(String, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
source_dialect = Column(String, nullable=False)
|
|
target_dialect = Column(String, nullable=False)
|
|
is_active = Column(Boolean, default=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
|
# #endregion TerminologyDictionary
|
|
|
|
|
|
# #region DictionaryEntry [TYPE Class]
|
|
# @BRIEF A single term mapping entry with case-insensitive unique constraint on source_term_normalized and origin tracking.
|
|
class DictionaryEntry(Base):
|
|
__tablename__ = "dictionary_entries"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
source_term = Column(String, nullable=False)
|
|
source_term_normalized = Column(String, nullable=False)
|
|
target_term = Column(String, nullable=False)
|
|
source_language = Column(String, nullable=False, comment="BCP-47 source language code")
|
|
target_language = Column(String, nullable=False, comment="BCP-47 target language code")
|
|
context_notes = Column(Text, nullable=True)
|
|
context_data = Column(JSON, nullable=True, comment="Structured context for term usage")
|
|
usage_notes = Column(Text, nullable=True, comment="Usage guidance for the term mapping")
|
|
has_context = Column(Boolean, default=False, comment="Whether context_data is populated")
|
|
context_source = Column(String, nullable=True, comment="auto|auto_with_edits|manual|bulk")
|
|
origin_source_language = Column(String, nullable=True, comment="Original source language of the term")
|
|
origin_run_id = Column(String, nullable=True, comment="Run ID from which this correction originated")
|
|
origin_row_key = Column(String, nullable=True, comment="Row key within the run that triggered this correction")
|
|
origin_user_id = Column(String, nullable=True, comment="User who submitted the correction")
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"dictionary_id", "source_term_normalized", "source_language", "target_language",
|
|
name="uq_dict_source_term_lang"
|
|
),
|
|
Index("idx_dict_entry_lang", "source_language", "target_language"),
|
|
Index("idx_dict_has_context", "has_context"),
|
|
)
|
|
# #endregion DictionaryEntry
|
|
|
|
|
|
# #region TranslationSchedule [TYPE Class]
|
|
# @BRIEF Defines a cron-based schedule for recurring translation jobs.
|
|
class TranslationSchedule(Base):
|
|
__tablename__ = "translation_schedules"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
cron_expression = Column(String, nullable=False)
|
|
timezone = Column(String, nullable=False, default="UTC")
|
|
is_active = Column(Boolean, default=True)
|
|
last_run_at = Column(DateTime, nullable=True)
|
|
next_run_at = Column(DateTime, nullable=True)
|
|
execution_mode = Column(String, nullable=False, default="full", comment="full, new_key_only")
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
|
# #endregion TranslationSchedule
|
|
|
|
|
|
# #region TranslationJobDictionary [TYPE Class]
|
|
# @BRIEF Many-to-many association between translation jobs and terminology dictionaries.
|
|
class TranslationJobDictionary(Base):
|
|
__tablename__ = "translation_job_dictionaries"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"job_id", "dictionary_id",
|
|
name="uq_job_dictionary"
|
|
),
|
|
)
|
|
# #endregion TranslationJobDictionary
|
|
|
|
|
|
# #region MetricSnapshot [TYPE Class]
|
|
# @BRIEF Captures translation performance metrics at a point in time with config/content/dictionary hash keys for aggregation.
|
|
# @RATIONALE job_id is nullable with SET NULL because prune_expired creates aggregate snapshots
|
|
# (job_id=None) that span all jobs — enforced FK in PostgreSQL rejects virtual IDs like
|
|
# '_prune_aggregate_' (see [SEC:PG-FK]).
|
|
class MetricSnapshot(Base):
|
|
__tablename__ = "translation_metric_snapshots"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
key_hash = Column(String, nullable=False, comment="Hash of dimension key fields for aggregation")
|
|
config_hash = Column(String, nullable=True, comment="Hash of translation configuration state")
|
|
dict_snapshot_hash = Column(String, nullable=True, comment="Hash of dictionary state at capture time")
|
|
covers_events_before = Column(DateTime, nullable=True, comment="Indicates snapshot covers events before this timestamp")
|
|
total_jobs = Column(Integer, default=0)
|
|
total_runs = Column(Integer, default=0)
|
|
total_records = Column(Integer, default=0)
|
|
successful_records = Column(Integer, default=0)
|
|
failed_records = Column(Integer, default=0)
|
|
skipped_records = Column(Integer, default=0)
|
|
avg_duration_ms = Column(Integer, nullable=True)
|
|
p50_duration_ms = Column(Integer, nullable=True)
|
|
p95_duration_ms = Column(Integer, nullable=True)
|
|
p99_duration_ms = Column(Integer, nullable=True)
|
|
per_language_metrics = Column(JSON, nullable=True, comment="Per-language cumulative metrics: {lang: {cumulative_tokens, cumulative_cost, runs}}")
|
|
snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
|
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
|
|
__table_args__ = (
|
|
Index("ix_metric_snapshots_job_date", "job_id", "snapshot_date"),
|
|
)
|
|
# #endregion MetricSnapshot
|
|
|
|
|
|
# #region TranslationLanguage [C:1] [TYPE Class]
|
|
# @BRIEF Per-language translation result for a single record, supporting multi-language output.
|
|
class TranslationLanguage(Base):
|
|
__tablename__ = "translation_languages"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
record_id = Column(String, ForeignKey("translation_records.id", ondelete="CASCADE"), nullable=False)
|
|
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
|
source_language_detected = Column(String, nullable=True, comment="BCP-47 or 'und' for undetermined")
|
|
translated_value = Column(Text, nullable=True, comment="LLM-generated translation")
|
|
user_edit = Column(Text, nullable=True, comment="User-edited translation")
|
|
final_value = Column(Text, nullable=True, comment="Final resolved value (translated or user edit)")
|
|
status = Column(String, default="pending", comment="pending|translated|approved|edited|rejected|failed|skipped")
|
|
error_message = Column(Text, nullable=True)
|
|
needs_review = Column(Boolean, default=False, comment="Flagged because source language could not be determined")
|
|
language_overridden = Column(Boolean, default=False, comment="Source language was manually overridden by user")
|
|
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
|
|
|
record = relationship("TranslationRecord", back_populates="languages")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("record_id", "language_code", name="uq_record_language"),
|
|
Index("idx_tl_language", "language_code"),
|
|
Index("idx_tl_record_lang", "record_id", "language_code"),
|
|
)
|
|
# #endregion TranslationLanguage
|
|
|
|
|
|
# #region TranslationPreviewLanguage [C:1] [TYPE Class]
|
|
# @BRIEF Per-language preview entry within a preview session.
|
|
class TranslationPreviewLanguage(Base):
|
|
__tablename__ = "translation_preview_languages"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
preview_record_id = Column(String, ForeignKey("translation_preview_records.id", ondelete="CASCADE"), nullable=False)
|
|
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
|
source_language_detected = Column(String, nullable=True, comment="BCP-47 or 'und'")
|
|
translated_value = Column(Text, nullable=True)
|
|
user_edit = Column(Text, nullable=True)
|
|
final_value = Column(Text, nullable=True)
|
|
status = Column(String, default="pending", comment="pending|approved|edited|rejected")
|
|
needs_review = Column(Boolean, default=False, comment="Flagged because source language could not be determined")
|
|
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
|
|
|
preview_record = relationship("TranslationPreviewRecord", back_populates="languages")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("preview_record_id", "language_code", name="uq_preview_record_language"),
|
|
)
|
|
# #endregion TranslationPreviewLanguage
|
|
|
|
|
|
# #region TranslationRunLanguageStats [C:1] [TYPE Class]
|
|
# @BRIEF Per-language statistics for a translation run (row counts, tokens, cost).
|
|
class TranslationRunLanguageStats(Base):
|
|
__tablename__ = "translation_run_language_stats"
|
|
|
|
id = Column(String, primary_key=True, default=generate_uuid)
|
|
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False)
|
|
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
|
total_rows = Column(Integer, default=0)
|
|
translated_rows = Column(Integer, default=0)
|
|
failed_rows = Column(Integer, default=0)
|
|
skipped_rows = Column(Integer, default=0)
|
|
token_count = Column(Integer, default=0)
|
|
estimated_cost = Column(Float, default=0.0)
|
|
|
|
run = relationship("TranslationRun", back_populates="language_stats")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("run_id", "language_code", name="uq_run_language"),
|
|
Index("idx_rls_run", "run_id"),
|
|
)
|
|
# #endregion TranslationRunLanguageStats
|
|
|
|
|
|
# #endregion TranslateModels
|