Adds better method control, and overall stability. Adds valid_reference and valid_translation methods.
This commit is contained in:
parent
07f1c95d83
commit
8901087217
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
|
|||
|
||||
setup(
|
||||
name="getbible",
|
||||
version="1.0.2",
|
||||
version="1.0.3",
|
||||
author="Llewellyn van der Merwe",
|
||||
author_email="getbible@vdm.io",
|
||||
description="A Python package to retrieving Bible references with ease.",
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import os
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Union
|
||||
from getbible import GetBibleReference
|
||||
|
||||
|
||||
class GetBible:
|
||||
def __init__(self, repo_path="https://api.getbible.net", version='v2'):
|
||||
def __init__(self, repo_path: str = "https://api.getbible.net", version: str = 'v2') -> None:
|
||||
"""
|
||||
Initialize the GetBible class.
|
||||
|
||||
|
@ -24,49 +26,11 @@ class GetBible:
|
|||
self.__books_cache = {}
|
||||
self.__chapters_cache = {}
|
||||
self.__start_cache_reset_thread()
|
||||
self.__pattern = re.compile(r'^[a-zA-Z0-9]{1,30}$')
|
||||
# Determine if the repository path is a URL
|
||||
self.__repo_path_url = self.__repo_path.startswith("http://") or self.__repo_path.startswith("https://")
|
||||
|
||||
def __start_cache_reset_thread(self):
|
||||
"""
|
||||
Start a background thread to reset the cache monthly.
|
||||
|
||||
This method creates and starts a daemon thread that runs the cache reset function
|
||||
every month.
|
||||
"""
|
||||
reset_thread = threading.Thread(target=self.__reset_cache_monthly)
|
||||
reset_thread.daemon = True # Daemonize thread
|
||||
reset_thread.start()
|
||||
|
||||
def __reset_cache_monthly(self):
|
||||
"""
|
||||
Periodically clears the cache on the first day of each month.
|
||||
|
||||
This method runs in a background thread and calculates the time until the start
|
||||
of the next month. It sleeps until that time and then clears the cache.
|
||||
"""
|
||||
while True:
|
||||
time_to_sleep = self.__calculate_time_until_next_month()
|
||||
time.sleep(time_to_sleep)
|
||||
self.__chapters_cache.clear()
|
||||
print(f"Cache cleared on {datetime.now()}")
|
||||
|
||||
def __calculate_time_until_next_month(self):
|
||||
"""
|
||||
Calculate the seconds until the start of the next month.
|
||||
|
||||
Determines how many seconds are left until the first day of the next month
|
||||
from the current time. This duration is used by the cache reset thread to
|
||||
sleep until the cache needs to be cleared.
|
||||
|
||||
:return: Number of seconds until the start of the next month.
|
||||
"""
|
||||
now = datetime.now()
|
||||
# Calculate the first day of the next month
|
||||
first_of_next_month = (now.replace(day=1) + timedelta(days=32)).replace(day=1)
|
||||
return (first_of_next_month - now).total_seconds()
|
||||
|
||||
def select(self, reference, abbreviation='kjv'):
|
||||
def select(self, reference: str, abbreviation: Optional[str] = 'kjv') -> Dict[str, Union[Dict, str]]:
|
||||
"""
|
||||
Select and return Bible verses based on the reference and abbreviation.
|
||||
|
||||
|
@ -81,13 +45,13 @@ class GetBible:
|
|||
try:
|
||||
reference = self.__get.ref(ref, abbreviation)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid reference format.")
|
||||
raise ValueError(f"Invalid reference '{ref}'.")
|
||||
|
||||
self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result)
|
||||
|
||||
return result
|
||||
|
||||
def scripture(self, reference, abbreviation='kjv'):
|
||||
def scripture(self, reference: str, abbreviation: Optional[str] = 'kjv') -> str:
|
||||
"""
|
||||
Select and return Bible verses based on the reference and abbreviation.
|
||||
|
||||
|
@ -98,7 +62,72 @@ class GetBible:
|
|||
|
||||
return json.dumps(self.select(reference, abbreviation))
|
||||
|
||||
def __set_verse(self, abbreviation, book, chapter, verses, result):
|
||||
def valid_reference(self, reference: str, abbreviation: Optional[str] = 'kjv') -> bool:
|
||||
"""
|
||||
Validate a scripture reference and check its presence in the cache.
|
||||
|
||||
:param reference: Scripture reference string.
|
||||
:param abbreviation: Optional translation code.
|
||||
:return: True if valid and present, False otherwise.
|
||||
"""
|
||||
return self.__get.valid(reference, abbreviation)
|
||||
|
||||
def valid_translation(self, abbreviation: str) -> bool:
|
||||
"""
|
||||
Check if the given translation is valid.
|
||||
|
||||
:param abbreviation: The abbreviation of the Bible translation to check.
|
||||
:return: True if the translation is available, False otherwise.
|
||||
"""
|
||||
if self.__pattern.match(abbreviation):
|
||||
path = self.__generate_path(abbreviation, "books.json")
|
||||
# Check if the translation is already in the cache
|
||||
if abbreviation not in self.__books_cache:
|
||||
self.__books_cache[abbreviation] = self.__fetch_data(path)
|
||||
# Return True if the translation is available, False otherwise
|
||||
return self.__books_cache[abbreviation] is not None
|
||||
return False
|
||||
|
||||
def __start_cache_reset_thread(self) -> None:
|
||||
"""
|
||||
Start a background thread to reset the cache monthly.
|
||||
|
||||
This method creates and starts a daemon thread that runs the cache reset function
|
||||
every month.
|
||||
"""
|
||||
reset_thread = threading.Thread(target=self.__reset_cache_monthly)
|
||||
reset_thread.daemon = True # Daemonize thread
|
||||
reset_thread.start()
|
||||
|
||||
def __reset_cache_monthly(self) -> None:
|
||||
"""
|
||||
Periodically clears the cache on the first day of each month.
|
||||
|
||||
This method runs in a background thread and calculates the time until the start
|
||||
of the next month. It sleeps until that time and then clears the cache.
|
||||
"""
|
||||
while True:
|
||||
time_to_sleep = self.__calculate_time_until_next_month()
|
||||
time.sleep(time_to_sleep)
|
||||
self.__chapters_cache.clear()
|
||||
print(f"Cache cleared on {datetime.now()}")
|
||||
|
||||
def __calculate_time_until_next_month(self) -> float:
|
||||
"""
|
||||
Calculate the seconds until the start of the next month.
|
||||
|
||||
Determines how many seconds are left until the first day of the next month
|
||||
from the current time. This duration is used by the cache reset thread to
|
||||
sleep until the cache needs to be cleared.
|
||||
|
||||
:return: Number of seconds until the start of the next month.
|
||||
"""
|
||||
now = datetime.now()
|
||||
# Calculate the first day of the next month
|
||||
first_of_next_month = (now.replace(day=1) + timedelta(days=32)).replace(day=1)
|
||||
return (first_of_next_month - now).total_seconds()
|
||||
|
||||
def __set_verse(self, abbreviation: str, book: int, chapter: int, verses: list, result: Dict) -> None:
|
||||
"""
|
||||
Set verse information into the result JSON.
|
||||
:param abbreviation: Bible translation abbreviation.
|
||||
|
@ -131,20 +160,18 @@ class GetBible:
|
|||
result[cache_key] = {key: chapter_data[key] for key in chapter_data if key != "verses"}
|
||||
result[cache_key]["verses"] = [verse_info]
|
||||
|
||||
def __check_translation(self, abbreviation):
|
||||
def __check_translation(self, abbreviation: str) -> None:
|
||||
"""
|
||||
Check if the given translation is available.
|
||||
Check if the given translation is available and raises an exception if not found.
|
||||
|
||||
:param abbreviation: The abbreviation of the Bible translation to check.
|
||||
:raises FileNotFoundError: If the translation is not found.
|
||||
"""
|
||||
path = self.__generate_path(abbreviation, "books.json")
|
||||
if abbreviation not in self.__books_cache:
|
||||
self.__books_cache[abbreviation] = self.__fetch_data(path)
|
||||
if self.__books_cache[abbreviation] is None:
|
||||
raise FileNotFoundError(f"Translation ({abbreviation}) not found in this API.")
|
||||
# Use valid_translation to check if the translation is available
|
||||
if not self.valid_translation(abbreviation):
|
||||
raise FileNotFoundError(f"Translation ({abbreviation}) not found in this API.")
|
||||
|
||||
def __generate_path(self, abbreviation, file_name):
|
||||
def __generate_path(self, abbreviation: str, file_name: str) -> str:
|
||||
"""
|
||||
Generate the path or URL for a given file.
|
||||
|
||||
|
@ -157,7 +184,7 @@ class GetBible:
|
|||
else:
|
||||
return os.path.join(self.__repo_path, self.__repo_version, abbreviation, file_name)
|
||||
|
||||
def __fetch_data(self, path):
|
||||
def __fetch_data(self, path: str) -> Optional[Dict]:
|
||||
"""
|
||||
Fetch data from either a URL or a local file path.
|
||||
|
||||
|
@ -177,7 +204,7 @@ class GetBible:
|
|||
else:
|
||||
return None
|
||||
|
||||
def __retrieve_chapter_data(self, abbreviation, book, chapter):
|
||||
def __retrieve_chapter_data(self, abbreviation: str, book: int, chapter: int) -> Dict:
|
||||
"""
|
||||
Retrieve chapter data for a given book and chapter.
|
||||
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
from .getbible_reference_trie import GetBibleReferenceTrie
|
||||
import os
|
||||
from typing import Any, List, Optional
|
||||
|
||||
|
||||
class GetBibleBookNumber:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize the GetBibleBookNumber class.
|
||||
|
||||
Sets up the class by loading all translation tries from the data directory.
|
||||
"""
|
||||
self._tries = {}
|
||||
self._data_path = os.path.join(os.path.dirname(__file__), 'data')
|
||||
self._load_all_translations()
|
||||
self.__load_all_translations()
|
||||
|
||||
def _load_translation(self, filename):
|
||||
def __load_translation(self, filename: str) -> None:
|
||||
"""
|
||||
Load a translation trie from a specified file.
|
||||
|
||||
:param filename: The name of the file to load.
|
||||
:raises IOError: If there is an error loading the file.
|
||||
"""
|
||||
trie = GetBibleReferenceTrie()
|
||||
translation_code = filename.split('.')[0]
|
||||
try:
|
||||
|
@ -17,27 +29,38 @@ class GetBibleBookNumber:
|
|||
raise IOError(f"Error loading translation {translation_code}: {e}")
|
||||
self._tries[translation_code] = trie
|
||||
|
||||
def _load_all_translations(self):
|
||||
def __load_all_translations(self) -> None:
|
||||
"""
|
||||
Load all translation tries from the data directory.
|
||||
"""
|
||||
for filename in os.listdir(self._data_path):
|
||||
if filename.endswith('.json'):
|
||||
self._load_translation(filename)
|
||||
self.__load_translation(filename)
|
||||
|
||||
def number(self, reference, translation_code=None, fallback_translations=None):
|
||||
# Default to 'kjv' if no translation code is provided
|
||||
def number(self, reference: str, translation_code: Optional[str] = None,
|
||||
fallback_translations: Optional[List[str]] = None) -> Optional[int]:
|
||||
"""
|
||||
Get the book number based on a reference and translation code.
|
||||
|
||||
:param reference: The reference to search for.
|
||||
:param translation_code: The code for the translation to use.
|
||||
:param fallback_translations: A list of fallback translations to use if necessary.
|
||||
:return: The book number as an integer if found, None otherwise.
|
||||
"""
|
||||
if not translation_code or translation_code not in self._tries:
|
||||
translation_code = 'kjv'
|
||||
|
||||
translation = self._tries.get(translation_code)
|
||||
result = translation.search(reference) if translation else None
|
||||
if result:
|
||||
return result
|
||||
if result and result.isdigit():
|
||||
return int(result)
|
||||
|
||||
# If 'kjv' is not the original choice, try it next
|
||||
if translation_code != 'kjv':
|
||||
translation = self._tries.get('kjv')
|
||||
result = translation.search(reference) if translation else None
|
||||
if result:
|
||||
return result
|
||||
if result and result.isdigit():
|
||||
return int(result)
|
||||
|
||||
# Fallback to other translations
|
||||
if fallback_translations is None:
|
||||
|
@ -46,12 +69,19 @@ class GetBibleBookNumber:
|
|||
for code in fallback_translations:
|
||||
translation = self._tries.get(code)
|
||||
result = translation.search(reference) if translation else None
|
||||
if result:
|
||||
return result
|
||||
if result and result.isdigit():
|
||||
return int(result)
|
||||
|
||||
return None
|
||||
|
||||
def dump(self, translation_code, filename):
|
||||
def dump(self, translation_code: str, filename: str) -> None:
|
||||
"""
|
||||
Dump the trie data for a specific translation to a file.
|
||||
|
||||
:param translation_code: The code for the translation.
|
||||
:param filename: The name of the file to dump to.
|
||||
:raises ValueError: If no data is available for the specified translation.
|
||||
"""
|
||||
if translation_code in self._tries:
|
||||
self._tries[translation_code].dump(filename)
|
||||
else:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import re
|
||||
from getbible import GetBibleBookNumber
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -11,60 +12,156 @@ class BookReference:
|
|||
|
||||
|
||||
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, translation_code=None):
|
||||
# Split at the first colon to separate book from verses, defaulting to chapter 1, verse 1 if not present
|
||||
book_chapter, verses_portion = reference.split(':', 1) if ':' in reference else (reference, '1')
|
||||
# Try to extract the chapter number from the book_chapter part
|
||||
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)
|
||||
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)
|
||||
if chapter_match:
|
||||
# If a chapter number is found, extract it and the book name
|
||||
chapter_number = int(chapter_match.group())
|
||||
book_name = book_chapter[:chapter_match.start()].strip()
|
||||
else:
|
||||
# If no chapter number is found, default to chapter 1
|
||||
chapter_number = 1
|
||||
book_name = book_chapter.strip()
|
||||
# Retrieve the book number
|
||||
book_number = self.__get_book_number(book_name, translation_code)
|
||||
if not book_number:
|
||||
raise ValueError(f"Book number for '{book_name}' could not be found.")
|
||||
# Extract verses
|
||||
verses_arr = self.__get_verses_numbers(verses_portion.strip())
|
||||
# We return a dataclass (needs Python 3.7+)
|
||||
return BookReference(book=int(book_number), chapter=chapter_number, verses=verses_arr)
|
||||
return int(chapter_match.group()) if chapter_match else 1
|
||||
|
||||
def __get_verses_numbers(self, verses):
|
||||
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
|
||||
|
||||
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]
|
||||
# Process a string of verses into a list
|
||||
verse_parts = verses.split(',')
|
||||
verse_list = []
|
||||
for part in verse_parts:
|
||||
if '-' in part:
|
||||
range_parts = part.split('-')
|
||||
# Ignore if neither start nor end are digits
|
||||
if len(range_parts) == 2:
|
||||
start, end = range_parts
|
||||
if start.isdigit() and end.isdigit():
|
||||
verse_list.extend(range(int(start), int(end) + 1))
|
||||
elif start.isdigit():
|
||||
verse_list.append(int(start))
|
||||
elif len(range_parts) == 1 and range_parts[0].isdigit():
|
||||
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))
|
||||
if not verse_list:
|
||||
return [1]
|
||||
return verse_list
|
||||
return verse_list if verse_list else [1]
|
||||
|
||||
def __get_book_number(self, book_name, abbreviation):
|
||||
# Retrieve the book number given a translation abbreviation and a book name
|
||||
if re.match(r'^[0-9]+$', book_name):
|
||||
return book_name
|
||||
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.
|
||||
"""
|
||||
if book_name.isdigit():
|
||||
return int(book_name)
|
||||
book_number = self.__get_book.number(book_name, abbreviation)
|
||||
return book_number
|
||||
return int(book_number) if book_number is not None else None
|
||||
|
||||
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
|
||||
|
|
|
@ -1,31 +1,52 @@
|
|||
from .trie_node import TrieNode
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class GetBibleReferenceTrie:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize the GetBibleReferenceTrie class.
|
||||
|
||||
Sets up the Trie data structure for storing and searching book names.
|
||||
"""
|
||||
self.root = TrieNode()
|
||||
# Updated regex to support Unicode characters
|
||||
self.space_removal_regex = re.compile(r'(\d)\s+(\w)', re.UNICODE)
|
||||
|
||||
def _preprocess(self, name):
|
||||
# Remove all periods
|
||||
def __preprocess(self, name: str) -> str:
|
||||
"""
|
||||
Preprocess a book name by removing periods and spaces between numbers and words.
|
||||
|
||||
:param name: The book name to preprocess.
|
||||
:return: The processed name in lowercase.
|
||||
"""
|
||||
processed_name = name.replace('.', '')
|
||||
# Process the name considering Unicode characters
|
||||
processed_name = self.space_removal_regex.sub(r'\1\2', processed_name)
|
||||
return processed_name.lower()
|
||||
|
||||
def _insert(self, book_number, names):
|
||||
def __insert(self, book_number: str, names: [str]) -> None:
|
||||
"""
|
||||
Insert a book number with associated names into the Trie.
|
||||
|
||||
:param book_number: The book number to insert.
|
||||
:param names: A list of names associated with the book number.
|
||||
"""
|
||||
for name in names:
|
||||
processed_name = self._preprocess(name)
|
||||
processed_name = self.__preprocess(name)
|
||||
node = self.root
|
||||
for char in processed_name:
|
||||
node = node.children.setdefault(char, TrieNode())
|
||||
node.book_number = book_number
|
||||
|
||||
def search(self, book_name):
|
||||
processed_name = self._preprocess(book_name)
|
||||
def search(self, book_name: str) -> Optional[str]:
|
||||
"""
|
||||
Search for a book number based on a book name.
|
||||
|
||||
:param book_name: The book name to search for.
|
||||
:return: The book number if found, None otherwise.
|
||||
"""
|
||||
processed_name = self.__preprocess(book_name)
|
||||
node = self.root
|
||||
for char in processed_name:
|
||||
node = node.children.get(char)
|
||||
|
@ -33,7 +54,14 @@ class GetBibleReferenceTrie:
|
|||
return None
|
||||
return node.book_number if node.book_number else None
|
||||
|
||||
def _dump_to_dict(self, node=None, key=''):
|
||||
def __dump_to_dict(self, node: Optional[TrieNode] = None, key: str = '') -> Dict[str, Dict]:
|
||||
"""
|
||||
Convert the Trie into a dictionary representation.
|
||||
|
||||
:param node: The current Trie node to process.
|
||||
:param key: The current key being constructed.
|
||||
:return: Dictionary representation of the Trie.
|
||||
"""
|
||||
if node is None:
|
||||
node = self.root
|
||||
|
||||
|
@ -42,21 +70,34 @@ class GetBibleReferenceTrie:
|
|||
result[key] = {'book_number': node.book_number}
|
||||
|
||||
for char, child in node.children.items():
|
||||
result.update(self._dump_to_dict(child, key + char))
|
||||
result.update(self.__dump_to_dict(child, key + char))
|
||||
|
||||
return result
|
||||
|
||||
def dump(self, filename):
|
||||
trie_dict = self._dump_to_dict()
|
||||
def dump(self, filename: str) -> None:
|
||||
"""
|
||||
Dump the Trie data to a JSON file.
|
||||
|
||||
:param filename: The filename to dump the data to.
|
||||
"""
|
||||
trie_dict = self.__dump_to_dict()
|
||||
with open(filename, 'w') as file:
|
||||
json.dump(trie_dict, file, ensure_ascii=False, indent=4)
|
||||
|
||||
def load(self, file_path):
|
||||
def load(self, file_path: str) -> None:
|
||||
"""
|
||||
Load the Trie data from a JSON file.
|
||||
|
||||
:param file_path: The path of the file to load data from.
|
||||
:raises IOError: If there is an error opening the file.
|
||||
:raises ValueError: If there is an error decoding the JSON data.
|
||||
:raises Exception: If any other error occurs.
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r') as file:
|
||||
data = json.load(file)
|
||||
for book_number, names in data.items():
|
||||
self._insert(book_number, names)
|
||||
self.__insert(book_number, names)
|
||||
except IOError as e:
|
||||
raise IOError(f"Error loading file {file_path}: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
|
|
|
@ -8,48 +8,68 @@ class TestGetBibleBookNumber(unittest.TestCase):
|
|||
self.get_book = GetBibleBookNumber()
|
||||
|
||||
def test_valid_reference(self):
|
||||
self.assertEqual(self.get_book.number('Gen', 'kjv'), '1', "Failed to find 'Gen' in 'kjv' translation")
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('Gen', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen' in 'kjv' translation")
|
||||
|
||||
def test_valid_reference_ch(self):
|
||||
self.assertEqual(self.get_book.number('创世记', 'cns', ['cnt']), '1',
|
||||
"Failed to find '创世记' in 'cns' translation with 'cnt' fallback")
|
||||
self.assertEqual(self.get_book.number('创记', 'cus', ['cut']), '1',
|
||||
"Failed to find '创记' in 'cus' translation with 'cut' fallback")
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('创世记', 'cns', ['cnt'])
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创世记' in 'cns' translation with 'cnt' fallback")
|
||||
|
||||
def test_valid_reference__ch(self):
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('创记', 'cus', ['cut'])
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创记' in 'cus' translation with 'cut' fallback")
|
||||
|
||||
def test_valid_reference_ch_no_trans(self):
|
||||
self.assertEqual(self.get_book.number('创世记'), '1', "Failed to find '创世记' in 'none-given' translation")
|
||||
self.assertEqual(self.get_book.number('创记'), '1', "Failed to find '创记' in 'none-given' translation")
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('创世记')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创世记' in 'none-given' translation")
|
||||
|
||||
def test_valid_reference__ch_no_trans(self):
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('创记')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创记' in 'none-given' translation")
|
||||
|
||||
def test_valid_1_john(self):
|
||||
self.assertEqual(self.get_book.number('1 John', 'kjv'), '62', "Failed to find '1 John' in 'kjv' translation")
|
||||
expected_result = 62
|
||||
actual_result = self.get_book.number('1 John', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1 John' in 'kjv' translation")
|
||||
|
||||
def test_valid_1_peter_ch(self):
|
||||
self.assertEqual(self.get_book.number('彼得前书', 'cns'), '60',
|
||||
"Failed to find '彼得前书' in 'cns' translation")
|
||||
expected_result = 60
|
||||
actual_result = self.get_book.number('彼得前书', 'cns')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '彼得前书' in 'cns' translation")
|
||||
|
||||
def test_valid_first_john(self):
|
||||
self.assertEqual(self.get_book.number('First John', 'kjv'), '62',
|
||||
"Failed to find 'First John' in 'kjv' translation")
|
||||
expected_result = 62
|
||||
actual_result = self.get_book.number('First John', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find 'First John' in 'kjv' translation")
|
||||
|
||||
def test_valid_mismatch_nospace_call(self):
|
||||
self.assertEqual(self.get_book.number('1Jn', 'aov'), '62',
|
||||
"Failed to find '1Jn' in 'aov' translation with 'kjv' as fallback translation")
|
||||
expected_result = 62
|
||||
actual_result = self.get_book.number('1Jn', 'aov')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1Jn' in 'aov' translation with 'kjv' as fallback translation")
|
||||
|
||||
def test_valid_mismatch_call(self):
|
||||
self.assertEqual(self.get_book.number('1 John', 'aov'), '62',
|
||||
"Failed to find '1 John' in 'aov' translation with 'kjv' as fallback translation")
|
||||
expected_result = 62
|
||||
actual_result = self.get_book.number('1 John', 'aov')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1 John' in 'aov' translation with 'kjv' as fallback translation")
|
||||
|
||||
def test_invalid_reference(self):
|
||||
self.assertIsNone(self.get_book.number('NonExistent', 'kjv'),
|
||||
"Invalid reference 'NonExistent' unexpectedly found in 'kjv'")
|
||||
actual_result = self.get_book.number('NonExistent', 'kjv')
|
||||
self.assertIsNone(actual_result, "Invalid reference 'NonExistent' unexpectedly found in 'kjv'")
|
||||
|
||||
def test_nonexistent_translation(self):
|
||||
self.assertEqual(self.get_book.number('Gen', 'nonexistent', ['nonexistent', 'nonexistent']), '1',
|
||||
"Fallback to 'kjv' did not work for non-existent translation")
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('Gen', 'nonexistent', ['nonexistent', 'nonexistent'])
|
||||
self.assertEqual(actual_result, expected_result, "Fallback to 'kjv' did not work for non-existent translation")
|
||||
|
||||
def test_fallback_translation(self):
|
||||
self.assertEqual(self.get_book.number('Gen', 'bad-translation'), '1',
|
||||
"Fallback to 'kjv' did not work for 'bad-translation'")
|
||||
expected_result = 1
|
||||
actual_result = self.get_book.number('Gen', 'bad-translation')
|
||||
self.assertEqual(actual_result, expected_result, "Fallback to 'kjv' did not work for 'bad-translation'")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -8,51 +8,70 @@ class TestGetBibleReference(unittest.TestCase):
|
|||
self.get = GetBibleReference()
|
||||
|
||||
def test_valid_reference(self):
|
||||
self.assertEqual(self.get.ref('Gen 1:2-7', 'kjv'), BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7]),
|
||||
"Failed to find 'Gen 1:2-7' book reference.")
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7])
|
||||
actual_result = self.get.ref('Gen 1:2-7', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen 1:2-7' book reference.")
|
||||
|
||||
def test_valid_reference_ch(self):
|
||||
self.assertEqual(self.get.ref('创世记1:2-7', 'cns'),
|
||||
BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7]),
|
||||
"Failed to find '创世记1:2-7' book reference")
|
||||
self.assertEqual(self.get.ref('创记 1:2-', 'cus'), BookReference(book=1, chapter=1, verses=[2]),
|
||||
"Failed to find '创记 1:2-' book reference")
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7])
|
||||
actual_result = self.get.ref('创世记1:2-7', 'cns')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创世记1:2-7' book reference")
|
||||
|
||||
def test_valid_reference_missing_verse_ch(self):
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[2])
|
||||
actual_result = self.get.ref('创记 1:2-', 'cus')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:2-' book reference")
|
||||
|
||||
def test_valid_reference_missing_verse__ch(self):
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[5])
|
||||
actual_result = self.get.ref('创记 1:-5', 'cus')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:-5' book reference")
|
||||
|
||||
def test_valid_reference_ch_no_trans(self):
|
||||
self.assertEqual(self.get.ref('创世记'), BookReference(book=1, chapter=1, verses=[1]),
|
||||
"Failed to find '创世记 1:1' book reference")
|
||||
self.assertEqual(self.get.ref('创记'), BookReference(book=1, chapter=1, verses=[1]),
|
||||
"Failed to find '创记 1:1' book reference")
|
||||
actual_result = self.get.ref('创世记')
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[1])
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创世记 1:1' book reference")
|
||||
|
||||
def test_valid_reference_ch_no__trans(self):
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[1])
|
||||
actual_result = self.get.ref('创记')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:1' book reference")
|
||||
|
||||
def test_valid_1_john(self):
|
||||
self.assertEqual(self.get.ref('1 John', 'kjv'), BookReference(book=62, chapter=1, verses=[1]),
|
||||
"Failed to find '1 John 1:1' book reference")
|
||||
expected_result = BookReference(book=62, chapter=1, verses=[1])
|
||||
actual_result = self.get.ref('1 John', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1 John 1:1' book reference")
|
||||
|
||||
def test_valid_1_peter_ch(self):
|
||||
self.assertEqual(self.get.ref('彼得前书', 'cns'), BookReference(book=60, chapter=1, verses=[1]),
|
||||
"Failed to find '彼得前书 1:1' book reference")
|
||||
actual_result = self.get.ref('彼得前书', 'cns')
|
||||
expected_result = BookReference(book=60, chapter=1, verses=[1])
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '彼得前书 1:1' book reference")
|
||||
|
||||
def test_valid_first_john(self):
|
||||
self.assertEqual(self.get.ref('First John 3:16,19-21', 'kjv'),
|
||||
BookReference(book=62, chapter=3, verses=[16, 19, 20, 21]),
|
||||
"Failed to find 'First John 1:2-7' book reference.")
|
||||
expected_result = BookReference(book=62, chapter=3, verses=[16, 19, 20, 21])
|
||||
actual_result = self.get.ref('First John 3:16,19-21', 'kjv')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find 'First John 1:2-7' book reference.")
|
||||
|
||||
def test_valid_mismatch_nospace_call(self):
|
||||
self.assertEqual(self.get.ref('1Jn', 'aov'), BookReference(book=62, chapter=1, verses=[1]),
|
||||
"Failed to find '1Jn 1:1' book reference.")
|
||||
expected_result = BookReference(book=62, chapter=1, verses=[1])
|
||||
actual_result = self.get.ref('1Jn', 'aov')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1Jn 1:1' book reference.")
|
||||
|
||||
def test_valid_mismatch_call(self):
|
||||
self.assertEqual(self.get.ref('1 John 5', 'aov'), BookReference(book=62, chapter=5, verses=[1]),
|
||||
"Failed to find '1 John 5:1' book reference.")
|
||||
expected_result = BookReference(book=62, chapter=5, verses=[1])
|
||||
actual_result = self.get.ref('1 John 5', 'aov')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find '1 John 5:1' book reference.")
|
||||
|
||||
def test_invalid_reference(self):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
expected_exception = "Invalid reference 'NonExistent'."
|
||||
with self.assertRaises(ValueError) as actual:
|
||||
self.get.ref('NonExistent', 'kjv')
|
||||
self.assertEqual(str(context.exception), "Book number for 'NonExistent' could not be found.")
|
||||
self.assertEqual(str(actual.exception), expected_exception)
|
||||
|
||||
def test_nonexistent_translation(self):
|
||||
self.assertEqual(self.get.ref('Gen', 'nonexistent'), BookReference(book=1, chapter=1, verses=[1]),
|
||||
"Failed to find 'Gen 1:1' book reference.")
|
||||
expected_result = BookReference(book=1, chapter=1, verses=[1])
|
||||
actual_result = self.get.ref('Gen', 'nonexistent')
|
||||
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen 1:1' book reference.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in New Issue
Block a user