"""Data loading utilities for Bible source texts.""" from __future__ import annotations import json from pathlib import Path from typing import Any from src.data.migrations import load_glossary_any_version from src.models.verse import ( BookMetadata, ChapterSummary, GlossaryTerm, Verse, VerseReference, ) class DataLoader: """Loads Bible source texts and metadata from JSON files.""" def __init__(self, data_dir: Path | str) -> None: """Initialize the data loader. Args: data_dir: Path to the data directory containing sources and metadata. """ self.data_dir = Path(data_dir) self.sources_dir = self.data_dir / "sources" self.metadata_dir = self.data_dir / "metadata" def _load_json(self, path: Path) -> dict[str, Any]: """Load a JSON file.""" if not path.exists(): return {} with path.open("r", encoding="utf-8") as f: return json.load(f) def load_greek_book(self, book: str) -> dict[str, str]: """Load Greek source text for a book. Returns: Dictionary mapping verse references to Greek text. """ path = self.sources_dir / "greek" / f"{book}.json" return self._load_json(path) def load_japanese_meiji(self, book: str) -> dict[str, str]: """Load Meiji translation for a book.""" path = self.sources_dir / "japanese" / "meiji" / f"{book}.json" return self._load_json(path) def load_japanese_taisho(self, book: str) -> dict[str, str]: """Load Taisho translation for a book.""" path = self.sources_dir / "japanese" / "taisho" / f"{book}.json" return self._load_json(path) def load_japanese_kougo(self, book: str) -> dict[str, str]: """Load Kougo-yaku translation for a book (1954-1955).""" path = self.sources_dir / "japanese" / "kougo" / f"{book}.json" return self._load_json(path) def load_english_kjv(self, book: str) -> dict[str, str]: """Load KJV translation for a book.""" path = self.sources_dir / "english" / "kjv" / f"{book}.json" return self._load_json(path) def load_english_asv(self, book: str) -> dict[str, str]: """Load ASV translation for a book.""" path = self.sources_dir / "english" / "asv" / f"{book}.json" return self._load_json(path) def load_hebrew_book(self, book: str) -> dict[str, str]: """Load Hebrew source text for a book (OT only). Returns: Dictionary mapping verse references to Hebrew text. """ path = self.sources_dir / "hebrew" / f"{book}.json" return self._load_json(path) def load_verse(self, reference: VerseReference) -> Verse: """Load a verse with all available source texts. Args: reference: The verse reference to load. Returns: Verse object with all available source texts populated. """ ref_key = f"{reference.chapter}:{reference.verse}" greek = self.load_greek_book(reference.book) hebrew = self.load_hebrew_book(reference.book) meiji = self.load_japanese_meiji(reference.book) taisho = self.load_japanese_taisho(reference.book) kougo = self.load_japanese_kougo(reference.book) kjv = self.load_english_kjv(reference.book) asv = self.load_english_asv(reference.book) greek_data = greek.get(ref_key, {}) greek_text = ( greek_data.get("text") if isinstance(greek_data, dict) else greek_data ) greek_translit = ( greek_data.get("translit") if isinstance(greek_data, dict) else None ) hebrew_data = hebrew.get(ref_key, {}) hebrew_text = ( hebrew_data.get("text") if isinstance(hebrew_data, dict) else hebrew_data ) if hebrew_text: # MorphHB marks morpheme boundaries with "/" inside words; strip # them so the prompt receives normal Hebrew text. hebrew_text = hebrew_text.replace("/", "") hebrew_translit = ( hebrew_data.get("translit") if isinstance(hebrew_data, dict) else None ) greek_words = greek_data.get("words") if isinstance(greek_data, dict) else None hebrew_words = hebrew_data.get("words") if isinstance(hebrew_data, dict) else None return Verse( reference=reference, greek_text=greek_text or None, greek_translit=greek_translit, hebrew_text=hebrew_text or None, hebrew_translit=hebrew_translit, greek_words=greek_words, hebrew_words=hebrew_words, japanese_meiji=meiji.get(ref_key), japanese_taisho=taisho.get(ref_key), japanese_kougo=kougo.get(ref_key), english_kjv=kjv.get(ref_key), english_asv=asv.get(ref_key), ) def load_chapter_verses(self, book: str, chapter: int) -> list[Verse]: """Load all verses for a chapter. Args: book: Book name. chapter: Chapter number. Returns: List of Verse objects for the chapter. """ greek = self.load_greek_book(book) hebrew = self.load_hebrew_book(book) meiji = self.load_japanese_meiji(book) taisho = self.load_japanese_taisho(book) kougo = self.load_japanese_kougo(book) kjv = self.load_english_kjv(book) asv = self.load_english_asv(book) # Find all verses in this chapter from any source verse_nums: set[int] = set() for source in [greek, hebrew, meiji, taisho, kougo, kjv, asv]: for key in source: parts = key.split(":") if len(parts) == 2 and int(parts[0]) == chapter: verse_nums.add(int(parts[1])) verses = [] for verse_num in sorted(verse_nums): ref = VerseReference(book=book, chapter=chapter, verse=verse_num) verses.append(self.load_verse(ref)) return verses def load_book_metadata(self, book: str) -> BookMetadata | None: """Load metadata for a book.""" path = self.metadata_dir / book / "book_context.json" data = self._load_json(path) if not data: return None return BookMetadata(**data) def load_chapter_summary(self, book: str, chapter: int) -> ChapterSummary | None: """Load summary for a chapter.""" path = self.metadata_dir / book / "chapter_summaries.json" data = self._load_json(path) chapter_data = data.get(str(chapter)) if not chapter_data: return None return ChapterSummary(book=book, chapter=chapter, **chapter_data) def load_glossary(self) -> list[GlossaryTerm]: """Load the theological term glossary. Supports both v1 and v2 glossary formats, auto-converting v1 on load. """ path = self.metadata_dir / "glossary.json" return load_glossary_any_version(path) def get_book_chapters(self, book: str) -> list[int]: """Get list of chapter numbers for a book. Determines chapters by scanning available source files. """ chapters: set[int] = set() # Check all source directories for source_path in [ self.sources_dir / "greek" / f"{book}.json", self.sources_dir / "hebrew" / f"{book}.json", self.sources_dir / "japanese" / "meiji" / f"{book}.json", self.sources_dir / "english" / "kjv" / f"{book}.json", ]: data = self._load_json(source_path) for key in data: # str.split always yields at least one part. try: chapters.add(int(key.split(":")[0])) except ValueError: continue return sorted(chapters)