166 lines
6.5 KiB
Python
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
|