Source code for flexicon.code.Scripture.ScrBookOperations

#
#   ScrBookOperations.py
#
#   Class: ScrBookOperations
#          Scripture book operations for FieldWorks Language Explorer
#          projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

# Import BaseOperations parent class
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable

# Import FLEx LCM types
from SIL.LCModel import (
    IScrBook,
    IScrBookFactory,
    IScrBookRepository,
    IScrSection,
    IScripture,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
)

# Import string utilities
from ..Shared.string_utils import normalize_match_key


[docs] class ScrBookOperations(BaseOperations): """ This class provides operations for managing Scripture books in a FieldWorks project. Scripture books represent individual books of the Bible (Genesis, Matthew, etc.) identified by canonical numbers (1-66 for standard Protestant canon). Each book contains sections with headings and paragraph content. This class should be accessed via FLExProject.ScrBooks property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all Scripture books for book in project.ScrBooks.GetAll(): title = project.ScrBooks.GetTitle(book) print(f"Book: {title}") # Create a new book (Genesis = 1) genesis = project.ScrBooks.Create(1, "Genesis") # Find book by canonical number matthew = project.ScrBooks.Find(40) # Matthew = 40 # Get sections in a book sections = project.ScrBooks.GetSections(genesis) project.CloseProject() """ def __init__(self, project): """ Initialize ScrBookOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all Scripture books in the project. This method returns an iterator over all IScrBook objects in the project, allowing iteration over all Scripture books. Yields: IScrBook: Each Scripture book object in the project Example: >>> for book in project.ScrBooks.GetAll(): ... title = project.ScrBooks.GetTitle(book) ... num = project.ScrBooks.GetCanonicalNum(book) ... print(f"{num}: {title}") 1: Genesis 2: Exodus 40: Matthew Notes: - Returns an iterator for memory efficiency - Books are returned in database order (not canonical order) - Use GetCanonicalNum() to get the book number - For sorted books, sort by canonical number See Also: Find, Create, GetCanonicalNum """ scripture = self.__GetScripture() if not scripture: return iter([]) return iter(scripture.ScriptureBooksOS) @OperationsMethod def Create(self, canonical_num, title=None): """ Create a new Scripture book with the specified canonical number. Args: canonical_num (int): The canonical book number (1-66 for Protestant canon) 1=Genesis, 2=Exodus, ... 40=Matthew, 41=Mark, ... 66=Revelation title (str, optional): Book title. If None, uses default title for the canonical number. Returns: IScrBook: The newly created Scripture book object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If canonical_num is None FP_ParameterError: If canonical_num is invalid or book already exists Example: >>> # Create Genesis >>> genesis = project.ScrBooks.Create(1, "Genesis") >>> print(project.ScrBooks.GetTitle(genesis)) Genesis >>> # Create Matthew (canonical number 40) >>> matthew = project.ScrBooks.Create(40, "Matthew") >>> # Create with default title >>> exodus = project.ScrBooks.Create(2) Notes: - Canonical numbers: 1-39 = OT, 40-66 = NT (Protestant canon) - Each canonical number can only be used once - Book is added to Scripture.ScriptureBooksOS - Book GUID is auto-generated - Title defaults to English name if not specified See Also: Delete, Find, GetCanonicalNum """ self._EnsureWriteEnabled() self._ValidateParam(canonical_num, "canonical_num") if not isinstance(canonical_num, int) or canonical_num < 1 or canonical_num > 66: raise FP_ParameterError(f"Canonical number must be an integer between 1 and 66, got {canonical_num}") scripture = self.__GetScripture() if not scripture: raise FP_ParameterError("Project does not have Scripture enabled") # Check if book already exists existing = self.Find(canonical_num) if existing: raise FP_ParameterError(f"Book with canonical number {canonical_num} already exists") with self._TransactionCM(f"Create book {canonical_num}"): # Create the new book using the factory factory = self.project.project.ServiceLocator.GetService(IScrBookFactory) new_book = factory.Create(scripture.ScriptureBooksOS, canonical_num) # Set title if provided if title: wsHandle = self.project.project.DefaultVernWs mkstr = TsStringUtils.MakeString(title, wsHandle) new_book.Title.set_String(wsHandle, mkstr) return new_book @OperationsMethod def Delete(self, book_or_hvo): """ Delete a Scripture book from the FLEx project. Args: book_or_hvo: Either an IScrBook object or its HVO (database ID) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If book_or_hvo is None FP_ParameterError: If book doesn't exist Example: >>> book = project.ScrBooks.Find(1) # Genesis >>> if book: ... project.ScrBooks.Delete(book) >>> # Delete by HVO >>> project.ScrBooks.Delete(12345) Warning: - This is a destructive operation - All sections and paragraphs will be deleted - Cannot be undone Notes: - Deletion cascades to all owned objects (sections, paragraphs, etc.) - The canonical number becomes available for reuse See Also: Create """ self._EnsureWriteEnabled() self._ValidateParam(book_or_hvo, "book_or_hvo") # Resolve to book object book = self.__ResolveObject(book_or_hvo) # Delete the book (LCM handles removal from repository) book.Delete() @OperationsMethod def Find(self, canonical_num): """ Find a Scripture book by its canonical number. Args: canonical_num (int): The canonical book number (1-66) Returns: IScrBook or None: The book object if found, None otherwise Raises: FP_NullParameterError: If canonical_num is None Example: >>> genesis = project.ScrBooks.Find(1) >>> if genesis: ... title = project.ScrBooks.GetTitle(genesis) ... print(f"Found: {title}") Found: Genesis >>> # Find Matthew (canonical number 40) >>> matthew = project.ScrBooks.Find(40) Notes: - Returns first match only (should be unique) - Returns None if not found (doesn't raise exception) - Canonical numbers: 1-39 = OT, 40-66 = NT See Also: FindByName, GetAll, GetCanonicalNum """ self._ValidateParam(canonical_num, "canonical_num") scripture = self.__GetScripture() if not scripture: return None # Search through all books for book in scripture.ScriptureBooksOS: if book.CanonicalNum == canonical_num: return book return None @OperationsMethod def FindByName(self, name): """ Find a Scripture book by its title/name. Args: name (str): The book title to search for (case-insensitive) Returns: IScrBook or None: The book object if found, None otherwise Raises: FP_NullParameterError: If name is None Example: >>> genesis = project.ScrBooks.FindByName("Genesis") >>> if genesis: ... num = project.ScrBooks.GetCanonicalNum(genesis) ... print(f"Canonical number: {num}") Canonical number: 1 >>> # Case-insensitive search >>> matthew = project.ScrBooks.FindByName("matthew") Notes: - Search is case-insensitive - Returns first match only - Returns None if not found - Searches in default vernacular writing system See Also: Find, GetTitle """ self._ValidateParam(name, "name") if not name or not name.strip(): return None scripture = self.__GetScripture() if not scripture: return None wsHandle = self.project.project.DefaultVernWs target = normalize_match_key(name, casefold=True) # Search through all books for book in scripture.ScriptureBooksOS: book_title = ITsString(book.Title.get_String(wsHandle)).Text if book_title and normalize_match_key(book_title, casefold=True) == target: return book return None # --- Book Properties --- @OperationsMethod def GetCanonicalNum(self, book_or_hvo): """ Get the canonical number of a Scripture book. Args: book_or_hvo: Either an IScrBook object or its HVO Returns: int: The canonical book number (1-66) Raises: FP_NullParameterError: If book_or_hvo is None Example: >>> genesis = project.ScrBooks.Find(1) >>> num = project.ScrBooks.GetCanonicalNum(genesis) >>> print(num) 1 >>> matthew = project.ScrBooks.FindByName("Matthew") >>> print(project.ScrBooks.GetCanonicalNum(matthew)) 40 Notes: - Canonical numbers: 1-39 = OT, 40-66 = NT (Protestant canon) - Number is immutable (set at creation) See Also: Create, Find """ self._ValidateParam(book_or_hvo, "book_or_hvo") book = self.__ResolveObject(book_or_hvo) return book.CanonicalNum @OperationsMethod def GetTitle(self, book_or_hvo, wsHandle=None): """ Get the title of a Scripture book. Args: book_or_hvo: Either an IScrBook object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: The book title (empty string if not set) Raises: FP_NullParameterError: If book_or_hvo is None Example: >>> genesis = project.ScrBooks.Find(1) >>> title = project.ScrBooks.GetTitle(genesis) >>> print(title) Genesis >>> # Get in specific writing system >>> title_fr = project.ScrBooks.GetTitle(genesis, ... project.WSHandle('fr')) Notes: - Returns empty string if title not set - Title can be in multiple writing systems See Also: SetTitle, FindByName """ self._ValidateParam(book_or_hvo, "book_or_hvo") book = self.__ResolveObject(book_or_hvo) wsHandle = self.__WSHandle(wsHandle) title = ITsString(book.Title.get_String(wsHandle)).Text return title or "" @OperationsMethod def SetTitle(self, book_or_hvo, title, wsHandle=None): """ Set the title of a Scripture book. Args: book_or_hvo: Either an IScrBook object or its HVO title (str): The new book title wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If book_or_hvo or title is None Example: >>> genesis = project.ScrBooks.Find(1) >>> project.ScrBooks.SetTitle(genesis, "Genesis") >>> # Set in specific writing system >>> project.ScrBooks.SetTitle(genesis, "Genèse", ... project.WSHandle('fr')) Notes: - Title can be set in multiple writing systems - Empty title is allowed See Also: GetTitle, Create """ self._EnsureWriteEnabled() self._ValidateParam(book_or_hvo, "book_or_hvo") self._ValidateParam(title, "title") book = self.__ResolveObject(book_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(title, wsHandle) book.Title.set_String(wsHandle, mkstr) # --- Section Management --- @OperationsMethod def GetSections(self, book_or_hvo): """ Get all sections in a Scripture book. Args: book_or_hvo: Either an IScrBook object or its HVO Returns: list: List of IScrSection objects (empty list if none) Raises: FP_NullParameterError: If book_or_hvo is None Example: >>> genesis = project.ScrBooks.Find(1) >>> sections = project.ScrBooks.GetSections(genesis) >>> for section in sections: ... heading = project.ScrSections.GetHeading(section) ... print(f"Section: {heading}") Notes: - Returns empty list if book has no sections - Sections are in database order - Use ScrSectionOperations for section-level operations See Also: ScrSectionOperations.GetAll """ self._ValidateParam(book_or_hvo, "book_or_hvo") book = self.__ResolveObject(book_or_hvo) return list(book.SectionsOS) # --- Private Helper Methods --- def __ResolveObject(self, book_or_hvo): """ Resolve HVO or object to IScrBook. Args: book_or_hvo: Either an IScrBook object or an HVO (int) Returns: IScrBook: The resolved book object Raises: FP_ParameterError: If HVO doesn't refer to a Scripture book """ if isinstance(book_or_hvo, int): obj = self.project.Object(book_or_hvo) if not isinstance(obj, IScrBook): raise FP_ParameterError("HVO does not refer to a Scripture book") return obj return book_or_hvo def __WSHandle(self, wsHandle): """ Get writing system handle, defaulting to vernacular WS. Args: wsHandle: Optional writing system handle Returns: int: The writing system handle """ if wsHandle is None: return self.project.project.DefaultVernWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultVernWs) def __GetScripture(self): """ Get the Scripture object from the project. Returns: IScripture or None: The Scripture object if available """ if not hasattr(self.project, "lp") or not self.project.lp: return None return self.project.lp.TranslatedScriptureOA