# #region DictionaryImportExport [C:3] [TYPE Class] [SEMANTICS dictionary, import, export, csv, migration] # @BRIEF Import/export entries as CSV/TSV with conflict detection, and migration of old entries. # @RELATION DEPENDS_ON -> [DictionaryEntry] # @RELATION DEPENDS_ON -> [TerminologyDictionary] import csv import io from typing import Any from sqlalchemy.orm import Session from ...core.logger import belief_scope, logger from ...models.translate import DictionaryEntry, TerminologyDictionary from ._utils import _detect_delimiter, _normalize_term class DictionaryImportExport: """Import/export dictionary entries as CSV/TSV, and migrate old-style entries.""" # region import_entries [TYPE Function] # @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution. @staticmethod def import_entries( db: Session, dict_id: str, content: str, delimiter: str | None = None, on_conflict: str = "overwrite", preview_only: bool = False, default_source_language: str | None = None, default_target_language: str | None = None, ) -> dict[str, Any]: with belief_scope("DictionaryImportExport.import_entries"): dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first() if not dictionary: raise ValueError(f"Dictionary not found: {dict_id}") if not delimiter: delimiter = _detect_delimiter(content) logger.reason("Detected delimiter", {"delimiter": repr(delimiter)}) if delimiter not in (",", "\t"): raise ValueError(f"Unsupported delimiter: {delimiter!r}. Use ',' or '\\t'.") reader = csv.DictReader(io.StringIO(content), delimiter=delimiter) required_fields = {"source_term", "target_term"} if not reader.fieldnames or not required_fields.issubset(reader.fieldnames): raise ValueError( f"CSV/TSV must have at least 'source_term' and 'target_term' columns. " f"Got: {reader.fieldnames}" ) result: dict[str, Any] = {"total": 0, "created": 0, "updated": 0, "skipped": 0, "errors": [], "preview": []} rows = list(reader) result["total"] = len(rows) for row_idx, row in enumerate(rows): try: source_term = row.get("source_term", "").strip() target_term = row.get("target_term", "").strip() context_notes = row.get("context_notes", "").strip() or None source_language = row.get("source_language", "").strip() or default_source_language or "und" target_language = row.get("target_language", "").strip() or default_target_language or "und" if not source_term or not target_term: result["errors"].append({"line": row_idx + 2, "error": "Both source_term and target_term are required", "row": row}) continue normalized = _normalize_term(source_term) if preview_only: existing = ( db.query(DictionaryEntry) .filter( DictionaryEntry.dictionary_id == dict_id, DictionaryEntry.source_term_normalized == normalized, DictionaryEntry.source_language == source_language, DictionaryEntry.target_language == target_language, ) .first() ) result["preview"].append({ "line": row_idx + 2, "source_term": source_term, "target_term": target_term, "context_notes": context_notes, "source_language": source_language, "target_language": target_language, "is_conflict": existing is not None, "existing_target_term": existing.target_term if existing else None, }) continue existing = ( db.query(DictionaryEntry) .filter( DictionaryEntry.dictionary_id == dict_id, DictionaryEntry.source_term_normalized == normalized, DictionaryEntry.source_language == source_language, DictionaryEntry.target_language == target_language, ) .first() ) if existing: if on_conflict == "overwrite": existing.source_term = source_term existing.target_term = target_term existing.context_notes = context_notes existing.source_language = source_language existing.target_language = target_language result["updated"] += 1 elif on_conflict == "keep_existing": result["skipped"] += 1 else: result["errors"].append({"line": row_idx + 2, "error": f"Conflict on '{source_term}' and on_conflict='cancel'", "row": row}) continue else: entry = DictionaryEntry( dictionary_id=dict_id, source_term=source_term, source_term_normalized=normalized, target_term=target_term, context_notes=context_notes, source_language=source_language, target_language=target_language, ) db.add(entry) result["created"] += 1 except Exception as e: result["errors"].append({"line": row_idx + 2, "error": str(e), "row": row}) if not preview_only: db.commit() logger.reflect("Import complete", {"dict_id": dict_id, "total": result["total"], "created": result["created"]}) return result # endregion import_entries # region export_entries [TYPE Function] # @PURPOSE: Export all entries as CSV string with language columns. @staticmethod def export_entries(db: Session, dict_id: str, delimiter: str = ",") -> str: with belief_scope("DictionaryImportExport.export_entries"): dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first() if not dictionary: raise ValueError(f"Dictionary not found: {dict_id}") entries = ( db.query(DictionaryEntry) .filter(DictionaryEntry.dictionary_id == dict_id) .order_by(DictionaryEntry.source_term.asc()) .all() ) output = io.StringIO() fieldnames = ["source_term", "target_term", "source_language", "target_language", "context_notes", "context_data", "usage_notes"] writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=delimiter) writer.writeheader() for entry in entries: writer.writerow({ "source_term": entry.source_term, "target_term": entry.target_term, "source_language": entry.source_language, "target_language": entry.target_language, "context_notes": entry.context_notes or "", "context_data": entry.context_data, "usage_notes": entry.usage_notes or "", }) result = output.getvalue() logger.reflect("Entries exported", {"dict_id": dict_id, "count": len(entries)}) return result # endregion export_entries # region migrate_old_entries [TYPE Function] # @PURPOSE: Migrate existing single-language dictionary entries to populate source_language and target_language. @staticmethod def migrate_old_entries(db: Session) -> dict[str, int]: with belief_scope("DictionaryImportExport.migrate_old_entries"): logger.reason("Starting migration of old dictionary entries") migrated_source = 0 migrated_target = 0 skipped = 0 entries = ( db.query(DictionaryEntry) .filter( (DictionaryEntry.source_language == "und") | (DictionaryEntry.target_language == "und") ) .all() ) for entry in entries: if entry.source_language == "und": if entry.origin_source_language: entry.source_language = entry.origin_source_language migrated_source += 1 if entry.source_language == "und" and entry.target_language == "und": skipped += 1 db.commit() logger.reflect("Migration complete", {"migrated_source": migrated_source, "total_processed": len(entries)}) return {"migrated_source": migrated_source, "migrated_target": migrated_target, "skipped": skipped, "total_processed": len(entries)} # endregion migrate_old_entries # #endregion DictionaryImportExport