5
0
librarian/src/getbible/getbible_reference.py

166 lines
6.5 KiB
Python

import re
from getbible import GetBibleBookNumber
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
class BookReference:
book: int
chapter: int
verses: list
reference: str
class GetBibleReference:
def __init__(self):
self.__get_book = GetBibleBookNumber()
self.__pattern = re.compile(r'[\w\s,:-]{1,50}', re.UNICODE)
self.__cache = {}
self.__cache_limit = 5000
def ref(self, reference: str, translation_code: Optional[str] = None) -> BookReference:
"""
Fetch the BookReference from cache or create it if not present.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: BookReference object.
:raises ValueError: If reference is invalid.
"""
sanitized_ref = self.__sanitize(reference)
if not sanitized_ref:
raise ValueError(f"Invalid reference '{reference}'.")
if sanitized_ref not in self.__cache:
book_ref = self.__book_reference(reference, translation_code)
if book_ref is None:
raise ValueError(f"Invalid reference '{reference}'.")
self.__manage_local_cache(sanitized_ref, book_ref)
return self.__cache[sanitized_ref]
def valid(self, reference: str, translation_code: Optional[str] = None) -> bool:
"""
Validate a scripture reference and check its presence in the cache.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: True if valid and present, False otherwise.
"""
sanitized_ref = self.__sanitize(reference)
if sanitized_ref is None:
return False
if sanitized_ref not in self.__cache:
book_ref = self.__book_reference(reference, translation_code)
self.__manage_local_cache(sanitized_ref, book_ref)
return self.__cache[sanitized_ref] is not None
def __sanitize(self, reference: str) -> Optional[str]:
"""
Sanitize a scripture reference by validating and escaping it.
:param reference: The scripture reference to sanitize.
:return: Sanitized reference or None if invalid.
"""
if self.__pattern.match(reference):
return re.escape(reference)
return None
def __book_reference(self, reference: str, translation_code: Optional[str] = None) -> Optional[BookReference]:
"""
Create a BookReference object from a scripture reference.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: BookReference object or None if invalid.
"""
try:
book_chapter, verses_portion = self.__split_reference(reference)
book_name = self.__extract_book_name(book_chapter)
book_number = self.__get_book_number(book_name, translation_code)
if not book_number:
return None
verses_arr = self.__get_verses_numbers(verses_portion)
chapter_number = self.__extract_chapter(book_chapter)
return BookReference(book=int(book_number), chapter=chapter_number, verses=verses_arr, reference=reference)
except Exception:
return None
def __split_reference(self, reference: str) -> Tuple[str, str]:
"""
Split a scripture reference into book chapter and verses portion.
:param reference: Scripture reference string.
:return: Tuple of book chapter and verses portion.
"""
return reference.split(':', 1) if ':' in reference else (reference, '1')
def __extract_chapter(self, book_chapter: str) -> int:
"""
Extract the chapter number from the book chapter part.
:param book_chapter: Book chapter part of the reference.
:return: Extracted chapter number.
"""
chapter_match = re.search(r'\d+$', book_chapter)
return int(chapter_match.group()) if chapter_match else 1
def __extract_book_name(self, book_chapter: str) -> str:
"""
Extract the book name from the book chapter part.
:param book_chapter: Book chapter part of the reference.
:return: Extracted book name.
"""
if book_chapter.isdigit():
# If the entire string is numeric, return it as is
return book_chapter.strip()
chapter_match = re.search(r'\d+$', book_chapter)
return book_chapter[:chapter_match.start()].strip() if chapter_match else book_chapter.strip()
def __get_verses_numbers(self, verses: str) -> list:
"""
Convert a verses portion of a reference into a list of verse numbers.
:param verses: Verses portion of the reference.
:return: List of verse numbers.
"""
if not verses:
return [1]
verse_parts = verses.split(',')
verse_list = []
for part in verse_parts:
if '-' in part:
range_parts = part.split('-')
if all(rp.isdigit() for rp in range_parts):
start, end = sorted(map(int, range_parts))
verse_list.extend(range(start, end + 1))
elif len(range_parts) == 2 and range_parts[0].isdigit() and not range_parts[1]:
verse_list.append(int(range_parts[0]))
elif len(range_parts) == 2 and range_parts[1].isdigit() and not range_parts[0]:
verse_list.append(int(range_parts[1]))
elif part.isdigit():
verse_list.append(int(part))
return verse_list if verse_list else [1]
def __get_book_number(self, book_name: str, abbreviation: Optional[str]) -> Optional[int]:
"""
Retrieve the book number given a book name and translation abbreviation.
:param book_name: Name of the book.
:param abbreviation: Translation abbreviation.
:return: Book number or None if not found.
"""
return self.__get_book.number(book_name, abbreviation)
def __manage_local_cache(self, key: str, value: Optional[BookReference]):
"""
Manage the insertion and eviction policy for the cache.
:param key: The key to insert into the cache.
:param value: The value to associate with the key.
"""
if len(self.__cache) >= self.__cache_limit:
self.__cache.pop(next(iter(self.__cache))) # Evict the oldest cache item
self.__cache[key] = value