# -*- coding: utf-8 -*-
"""
:File: EuljiroWorship/core/utils/bible_data_loader.py
:Author: Benjamin Jaedon Choi - https://github.com/saintbenjamin
:Affiliated Church: The Eulji-ro Presbyterian Church [대한예수교장로회(통합) 을지로교회]
:Address: The Eulji-ro Presbyterian Church, 24-10, Eulji-ro 20-gil, Jung-gu, Seoul 04549, South Korea
:Telephone: +82-2-2266-3070
:E-mail: euljirochurch [at] G.M.A.I.L. (replace [at] with @ and G.M.A.I.L as you understood.)
:License: MIT License with Attribution Requirement (see LICENSE file for details); Copyright (c) 2025 The Eulji-ro Presbyterian Church.
Handles lazy loading and caching of Bible text and metadata from JSON sources.
This module provides a unified data access layer for Bible-related resources,
including:
- Bible version aliases
- Book name aliases
- Canonical book names
- Custom sort order
- Full Bible text data
All Bible text JSON files are loaded lazily and cached in memory on first access
to minimize startup cost and disk I/O.
"""
import os
import json
from core.config import paths
from core.utils.logger import log_error
[docs]
class BibleDataLoader:
"""
Lazy-loading data manager for Bible text and metadata.
This loader provides on-demand (lazy) access to Bible JSON resources and caches
loaded content in memory to avoid repeated disk I/O.
It loads and exposes:
- Version alias metadata (e.g., full name → short label)
- Book alias / canonical naming metadata
- Canonical book name table (per language)
- Version sort order metadata
- Per-version Bible text JSON files (loaded only when first accessed)
The class is shared by both EuljiroBible and EuljiroWorship and therefore keeps
several compatibility helpers and legacy-style accessors.
Attributes:
json_dir (str):
Directory containing Bible metadata JSON files (aliases, canonical names,
and sort order).
text_dir (str):
Directory containing Bible text JSON files (per-version text).
aliases_version (dict):
Parsed content of ``aliases_version.json``. Typically maps a version key
to either a string alias or a nested dict containing localized aliases,
depending on your data schema.
aliases_book (dict):
Parsed content of ``aliases_book.json``. Used for book name aliasing and
compatibility mapping.
standard_book (dict):
Parsed content of ``standard_book.json``. Maps canonical book IDs to a
per-language display name dictionary (e.g., ``{"John": {"ko": "...", "en": "John"}}``).
sort_order (dict):
Parsed content of the configured sort-order JSON (e.g., prefix → rank).
Used by :meth:`get_sort_key` for stable version ordering in UI/CLI.
data (dict):
In-memory cache of loaded Bible text, keyed by version key.
Structure is typically ``data[version][book][chapter][verse] = text``.
Note:
- Version JSON is loaded lazily by :meth:`get_verses`. Use :meth:`load_version` or :meth:`load_versions` to preload explicitly.
- Some methods exist primarily for cross-project compatibility and are not necessarily used by every code path.
"""
[docs]
def __init__(self, json_dir=None, text_dir=None):
"""
Initialize the Bible data loader.
Optional directory overrides can be supplied for testing or
alternative data layouts.
Args:
json_dir (str, optional):
Directory containing Bible metadata JSON files
(aliases, canonical names, sort order).
text_dir (str, optional):
Directory containing Bible text JSON files.
Returns:
None
"""
self.json_dir = json_dir or paths.BIBLE_NAME_DIR
self.text_dir = text_dir or paths.BIBLE_DATA_DIR
# Load version and book aliases, canonical book names, and sort order
self.aliases_version = self._load_json(os.path.join(self.json_dir, "aliases_version.json"))
self.aliases_book = self._load_json(os.path.join(self.json_dir, "aliases_book.json"))
self.standard_book = self._load_json(os.path.join(self.json_dir, "standard_book.json"))
self.sort_order = self._load_json(os.path.join(self.json_dir, "your_sort_order.json"))
self.data = {} # Cache for loaded Bible texts
[docs]
def get_verses(self, version):
"""
Retrieve all verses for a given Bible version.
The version data is loaded from disk only once and cached
internally for subsequent access.
Args:
version (str):
Bible version key (e.g. "NKRV", "KJV").
Returns:
dict:
Nested dictionary structure containing the full Bible
text for the specified version.
"""
if version not in self.data:
try:
with open(os.path.join(self.text_dir, f"{version}.json"), "r", encoding="utf-8") as f:
self.data[version] = json.load(f)
except Exception as e:
log_error(f"[BibleDataLoader] Failed to load version '{version}': {e}")
self.data[version] = {}
return self.data[version]
[docs]
def get_books_for_version(self, version):
"""
Return the list of books available in a given Bible version.
Args:
version (str):
Bible version key.
Returns:
list:
List of book identifiers present in the version.
"""
verses = self.get_verses(version)
return list(verses.keys()) if verses else []
[docs]
def _load_json(self, file_path):
"""
Safely load a JSON file and return its contents.
If loading fails, an empty dictionary is returned and a warning
is printed to the console.
Args:
file_path (str):
Absolute path to the JSON file.
Returns:
dict:
Parsed JSON content, or an empty dict on failure.
"""
try:
with open(file_path, encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[Warning] Failed to load {file_path}: {e}")
return {}
[docs]
def get_standard_book(self, book_id: str, lang_code: str) -> str:
"""
Return the localized canonical name of a Bible book.
Args:
book_id (str):
Canonical book identifier (e.g. "John").
lang_code (str):
Language code (e.g. "ko", "en").
Returns:
str:
Localized book name if available, otherwise the book ID.
"""
return self.standard_book.get(book_id, {}).get(lang_code, book_id)
[docs]
def get_sort_key(self):
"""
Return a sorting key function for Bible version names.
The key function sorts versions using a predefined regional
prefix order followed by lexicographical ordering.
This method is required by both GUI and CLI code paths.
*DO NOT DELETE*.
Args:
None
Returns:
function:
Callable suitable for use as the ``key`` argument in ``sorted()``.
"""
def sort_key(version_name: str):
prefix = version_name.split()[0]
return (self.sort_order.get(prefix, 99), version_name)
return sort_key
[docs]
def load_version(self, version_key):
"""
Explicitly load a Bible version into memory.
This method forces loading even if lazy access has not yet occurred.
Args:
version_key (str):
Bible version key (filename without extension).
Returns:
None
"""
path = os.path.join(self.text_dir, f"{version_key}.json")
try:
with open(path, "r", encoding="utf-8") as f:
self.data[version_key] = json.load(f)
except Exception as e:
print(f"[ERROR] Failed to load version {version_key}: {e}")
self.data[version_key] = {}
[docs]
def load_versions(self, target_versions=None):
"""
Load multiple Bible versions into memory.
If no target list is provided, all available versions
in the text directory are loaded.
Args:
target_versions (list, optional):
List of version keys to load.
Returns:
None
"""
if target_versions is None:
self.data.clear()
target_versions = [
fname.replace(".json", "")
for fname in os.listdir(self.text_dir)
if fname.endswith(".json")
]
for v in target_versions:
self.load_version(v)
[docs]
def get_max_verse(self, version, book, chapter):
"""
Return the maximum verse number for a given chapter.
Args:
version (str):
Bible version key.
book (str):
Book identifier.
chapter (int):
Chapter number.
Returns:
int:
Maximum verse number, or 0 if unavailable.
"""
version_data = self.data.get(version)
if not version_data:
return 0
book_data = version_data.get(book)
if not book_data:
return 0
chapter_data = book_data.get(str(chapter))
if not chapter_data:
return 0
return max(map(int, chapter_data.keys()), default=0)
[docs]
def get_verses_for_display(self, versions=None, book=None, chapter=None, verse_range=None):
"""
Return either a filtered verse subset or full Bible data.
Args:
versions (list, optional):
Bible version keys.
book (str, optional):
Book identifier.
chapter (int, optional):
Chapter number.
verse_range (tuple, optional):
Verse range.
Returns:
dict:
Structured Bible text data.
"""
if versions and book and chapter and verse_range:
return self.extract_verses(versions, book, chapter, verse_range)
else:
return self.get_verses()
[docs]
def get_book_alias(self, lang_code="ko") -> dict:
"""
Return mapping of book IDs to localized aliases.
Args:
lang_code (str):
Language code.
Returns:
dict:
Mapping of ``book_id`` -> localized name.
"""
return {
book_id: data.get(lang_code, book_id)
for book_id, data in self.standard_book.items()
}
[docs]
def get_version_alias(self, lang_code="ko") -> dict:
"""
Return mapping of Bible version keys to localized aliases.
Args:
lang_code (str):
Language code.
Returns:
dict:
Mapping of ``version_key`` -> alias string.
"""
alias_map = {}
for k, v in self.aliases_version.items():
if isinstance(v, dict):
alias_map[k] = v.get("aliases", {}).get(lang_code, k)
else:
alias_map[k] = v
return alias_map
# For compatibility with EuljiroWorship system
[docs]
def get_verse(self, version: str, book: str, chapter: int, verse: int) -> str | None:
"""
Retrieve a single verse from the loaded Bible data.
This method exists for compatibility with the EuljiroWorship system.
Args:
version (str):
Bible version key.
book (str):
Book identifier.
chapter (int):
Chapter number.
verse (int):
Verse number.
Returns:
str | None:
Verse text if found, otherwise None.
"""
verses = self.get_verses(version)
return (
verses.get(book, {})
.get(str(chapter), {})
.get(str(verse))
)