Skip to content

Commit

Permalink
Adds book reference to API output. Improved overall stability. Adds m…
Browse files Browse the repository at this point in the history
…ore tests to check stability of code base.
  • Loading branch information
Llewellynvdm committed Nov 28, 2023
1 parent 8901087 commit 8e28a9f
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 64 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="getbible",
version="1.0.3",
version="1.1.0",
author="Llewellyn van der Merwe",
author_email="getbible@vdm.io",
description="A Python package to retrieving Bible references with ease.",
Expand Down
30 changes: 17 additions & 13 deletions src/getbible/getbible.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import datetime, timedelta
from typing import Dict, Optional, Union
from getbible import GetBibleReference
from getbible import BookReference


class GetBible:
Expand All @@ -26,6 +27,7 @@ def __init__(self, repo_path: str = "https://api.getbible.net", version: str = '
self.__books_cache = {}
self.__chapters_cache = {}
self.__start_cache_reset_thread()
# Pattern to check valid translations names
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://")
Expand All @@ -43,11 +45,11 @@ def select(self, reference: str, abbreviation: Optional[str] = 'kjv') -> Dict[st
references = reference.split(';')
for ref in references:
try:
reference = self.__get.ref(ref, abbreviation)
book_reference = self.__get.ref(ref, abbreviation)
except ValueError:
raise ValueError(f"Invalid reference '{ref}'.")

self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result)
self.__set_verse(abbreviation, book_reference, result)

return result

Expand Down Expand Up @@ -127,37 +129,39 @@ def __calculate_time_until_next_month(self) -> float:
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:
def __set_verse(self, abbreviation: str, book_ref: BookReference, result: Dict) -> None:
"""
Set verse information into the result JSON.
:param abbreviation: Bible translation abbreviation.
:param book: The book of the Bible.
:param chapter: The chapter number.
:param verses: List of verse numbers.
:param book_ref: The book reference class.
:param result: The dictionary to store verse information.
"""
cache_key = f"{abbreviation}_{book}_{chapter}"
cache_key = f"{abbreviation}_{book_ref.book}_{book_ref.chapter}"
if cache_key not in self.__chapters_cache:
chapter_data = self.__retrieve_chapter_data(abbreviation, book, chapter)
chapter_data = self.__retrieve_chapter_data(abbreviation, book_ref.book, book_ref.chapter)
# Convert verses list to dictionary for faster lookup
verse_dict = {str(v["verse"]): v for v in chapter_data.get("verses", [])}
chapter_data["verses"] = verse_dict
self.__chapters_cache[cache_key] = chapter_data
else:
chapter_data = self.__chapters_cache[cache_key]

for verse in verses:
for verse in book_ref.verses:
verse_info = chapter_data["verses"].get(str(verse))
if not verse_info:
raise ValueError(f"Verse {verse} not found in book {book}, chapter {chapter}.")
raise ValueError(f"Verse {verse} not found in book {book_ref.book}, chapter {book_ref.chapter}.")

if cache_key in result:
existing_verses = {str(v["verse"]) for v in result[cache_key].get("verses", [])}
if str(verse) not in existing_verses:
result[cache_key]["verses"].append(verse_info)
existing_ref = result[cache_key].get("ref", [])
if str(book_ref.reference) not in existing_ref:
result[cache_key]["ref"].append(book_ref.reference)
else:
# Include all other relevant elements of your JSON structure
result[cache_key] = {key: chapter_data[key] for key in chapter_data if key != "verses"}
result[cache_key]["ref"] = [book_ref.reference]
result[cache_key]["verses"] = [verse_info]

def __check_translation(self, abbreviation: str) -> None:
Expand All @@ -169,7 +173,7 @@ def __check_translation(self, abbreviation: str) -> None:
"""
# 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.")
raise FileNotFoundError(f"Translation ({abbreviation}) not found.")

def __generate_path(self, abbreviation: str, file_name: str) -> str:
"""
Expand Down Expand Up @@ -210,13 +214,13 @@ def __retrieve_chapter_data(self, abbreviation: str, book: int, chapter: int) ->
:param abbreviation: Bible translation abbreviation.
:param book: The book of the Bible.
:param chapter: The chapter number.
:param chapter: The chapter.
:return: Chapter data.
:raises FileNotFoundError: If the chapter data is not found.
"""
chapter_file = f"{str(book)}/{chapter}.json" if self.__repo_path_url else os.path.join(str(book),
f"{chapter}.json")
chapter_data = self.__fetch_data(self.__generate_path(abbreviation, chapter_file))
if chapter_data is None:
raise FileNotFoundError(f"File {abbreviation}/{book}/{chapter}.json does not exist.")
raise FileNotFoundError(f"Chapter:{chapter} in book:{book} for {abbreviation} not found.")
return chapter_data
41 changes: 29 additions & 12 deletions src/getbible/getbible_book_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def __init__(self) -> None:
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.__tries = {}
self.__data_path = os.path.join(os.path.dirname(__file__), 'data')
self.__load_all_translations()

def __load_translation(self, filename: str) -> None:
Expand All @@ -24,19 +24,33 @@ def __load_translation(self, filename: str) -> None:
trie = GetBibleReferenceTrie()
translation_code = filename.split('.')[0]
try:
trie.load(os.path.join(self._data_path, filename))
trie.load(os.path.join(self.__data_path, filename))
except IOError as e:
raise IOError(f"Error loading translation {translation_code}: {e}")
self._tries[translation_code] = trie
self.__tries[translation_code] = trie

def __load_all_translations(self) -> None:
"""
Load all translation tries from the data directory.
"""
for filename in os.listdir(self._data_path):
for filename in os.listdir(self.__data_path):
if filename.endswith('.json'):
self.__load_translation(filename)

def __valid_book_number(self, number: str) -> Optional[int]:
"""
Check if the number is a valid book number.
"""
try:
num = int(number)
if 1 <= num <= 83:
return num
else:
return None
except ValueError:
# Handle the case where the number cannot be converted to an integer
return None

def number(self, reference: str, translation_code: Optional[str] = None,
fallback_translations: Optional[List[str]] = None) -> Optional[int]:
"""
Expand All @@ -47,27 +61,30 @@ def number(self, reference: str, translation_code: Optional[str] = None,
: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:
if reference.isdigit():
return self.__valid_book_number(reference)

if not translation_code or translation_code not in self.__tries:
translation_code = 'kjv'

translation = self._tries.get(translation_code)
translation = self.__tries.get(translation_code)
result = translation.search(reference) if translation else None
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')
translation = self.__tries.get('kjv')
result = translation.search(reference) if translation else None
if result and result.isdigit():
return int(result)

# Fallback to other translations
if fallback_translations is None:
fallback_translations = [code for code in self._tries if code != translation_code]
fallback_translations = [code for code in self.__tries if code != translation_code]

for code in fallback_translations:
translation = self._tries.get(code)
translation = self.__tries.get(code)
result = translation.search(reference) if translation else None
if result and result.isdigit():
return int(result)
Expand All @@ -82,7 +99,7 @@ def dump(self, translation_code: str, filename: str) -> None:
: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)
if translation_code in self.__tries:
self.__tries[translation_code].dump(filename)
else:
raise ValueError(f"No data available for translation: {translation_code}")
28 changes: 13 additions & 15 deletions src/getbible/getbible_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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
self.__cache = {}
self.__cache_limit = 5000

def ref(self, reference: str, translation_code: Optional[str] = None) -> BookReference:
"""
Expand All @@ -30,12 +31,12 @@ def ref(self, reference: str, translation_code: Optional[str] = None) -> BookRef
sanitized_ref = self.__sanitize(reference)
if not sanitized_ref:
raise ValueError(f"Invalid reference '{reference}'.")
if sanitized_ref not in self.cache:
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]
return self.__cache[sanitized_ref]

def valid(self, reference: str, translation_code: Optional[str] = None) -> bool:
"""
Expand All @@ -48,10 +49,10 @@ def valid(self, reference: str, translation_code: Optional[str] = None) -> bool:
sanitized_ref = self.__sanitize(reference)
if sanitized_ref is None:
return False
if sanitized_ref not in self.cache:
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
return self.__cache[sanitized_ref] is not None

def __sanitize(self, reference: str) -> Optional[str]:
"""
Expand Down Expand Up @@ -80,7 +81,7 @@ def __book_reference(self, reference: str, translation_code: Optional[str] = Non
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)
return BookReference(book=int(book_number), chapter=chapter_number, verses=verses_arr, reference=reference)
except Exception:
return None

Expand Down Expand Up @@ -112,7 +113,7 @@ def __extract_book_name(self, book_chapter: str) -> str:
"""
if book_chapter.isdigit():
# If the entire string is numeric, return it as is
return book_chapter
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()
Expand Down Expand Up @@ -150,10 +151,7 @@ def __get_book_number(self, book_name: str, abbreviation: Optional[str]) -> Opti
: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 int(book_number) if book_number is not None else None
return self.__get_book.number(book_name, abbreviation)

def __manage_local_cache(self, key: str, value: Optional[BookReference]):
"""
Expand All @@ -162,6 +160,6 @@ def __manage_local_cache(self, key: str, value: Optional[BookReference]):
: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
if len(self.__cache) >= self.__cache_limit:
self.__cache.pop(next(iter(self.__cache))) # Evict the oldest cache item
self.__cache[key] = value
12 changes: 6 additions & 6 deletions src/getbible/getbible_reference_trie.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def __init__(self) -> None:
Sets up the Trie data structure for storing and searching book names.
"""
self.root = TrieNode()
self.space_removal_regex = re.compile(r'(\d)\s+(\w)', re.UNICODE)
self.__root = TrieNode()
self.__space_removal_regex = re.compile(r'(\d)\s+(\w)', re.UNICODE)

def __preprocess(self, name: str) -> str:
"""
Expand All @@ -22,7 +22,7 @@ def __preprocess(self, name: str) -> str:
:return: The processed name in lowercase.
"""
processed_name = name.replace('.', '')
processed_name = self.space_removal_regex.sub(r'\1\2', processed_name)
processed_name = self.__space_removal_regex.sub(r'\1\2', processed_name)
return processed_name.lower()

def __insert(self, book_number: str, names: [str]) -> None:
Expand All @@ -34,7 +34,7 @@ def __insert(self, book_number: str, names: [str]) -> None:
"""
for name in names:
processed_name = self.__preprocess(name)
node = self.root
node = self.__root
for char in processed_name:
node = node.children.setdefault(char, TrieNode())
node.book_number = book_number
Expand All @@ -47,7 +47,7 @@ def search(self, book_name: str) -> Optional[str]:
:return: The book number if found, None otherwise.
"""
processed_name = self.__preprocess(book_name)
node = self.root
node = self.__root
for char in processed_name:
node = node.children.get(char)
if node is None:
Expand All @@ -63,7 +63,7 @@ def __dump_to_dict(self, node: Optional[TrieNode] = None, key: str = '') -> Dict
:return: Dictionary representation of the Trie.
"""
if node is None:
node = self.root
node = self.__root

result = {}
if node.book_number is not None:
Expand Down

0 comments on commit 8e28a9f

Please sign in to comment.