5
0

Adds efficient data retrieval, query optimization, for server performance. Streamlines the caching mechanism and ensures that the methods are efficient and robust. Clear the caching once a month to keep scripture in sync with API. Adds workflow testing.

This commit is contained in:
Llewellyn van der Merwe 2023-11-14 07:43:43 +02:00
parent 25e720788a
commit 270c8f3d48
Signed by: Llewellyn
GPG Key ID: A9201372263741E7
4 changed files with 179 additions and 23 deletions

37
.github/workflows/python-unittest.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Python Unittest
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install the package
run: |
pip install -e .
- name: Run unittests
run: |
python -m unittest discover -s tests

View File

@ -1,11 +1,8 @@
from setuptools import setup, find_packages
with open('requirements.txt') as f:
required = f.read().splitlines()
setup(
name="getBible-librarian",
version="0.2.0",
version="0.2.1",
author="Llewellyn van der Merwe",
author_email="getbible@vdm.io",
description="A Python package to retrieving Bible references with ease.",
@ -16,7 +13,10 @@ setup(
packages=find_packages(where="src"),
package_data={"getbible": ["data/*.json"]},
include_package_data=True,
install_requires=required,
install_requires=[
"requests~=2.31.0",
"setuptools>=65.5.1"
],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",

View File

@ -1,6 +1,9 @@
import os
import json
import requests
import threading
import time
from datetime import datetime, timedelta
from getbible import GetBibleReference
@ -9,6 +12,9 @@ class GetBible:
"""
Initialize the GetBible class.
Sets up the class by initializing the cache, starting the background thread for
monthly cache reset, and other necessary setups.
:param repo_path: The repository path, which can be a URL or a local file path.
:param version: The version of the Bible repository.
"""
@ -17,9 +23,49 @@ class GetBible:
self.__repo_version = version
self.__books_cache = {}
self.__chapters_cache = {}
self.__start_cache_reset_thread()
# 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'):
"""
Select and return Bible verses based on the reference and abbreviation.
@ -37,8 +83,7 @@ class GetBible:
except ValueError:
raise ValueError(f"Invalid reference format.")
for verse in reference.verses:
self.__set_verse(abbreviation, reference.book, reference.chapter, verse, result)
self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result)
return result
@ -53,32 +98,38 @@ class GetBible:
return json.dumps(self.select(reference, abbreviation))
def __set_verse(self, abbreviation, book, chapter, verse, result):
def __set_verse(self, abbreviation, book, chapter, verses, result):
"""
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 verse: The verse number.
:param verses: List of verse numbers.
:param result: The dictionary to store verse information.
"""
cache_key = f"{abbreviation}_{book}_{chapter}"
if cache_key not in self.__chapters_cache:
self.__chapters_cache[cache_key] = self.__retrieve_chapter_data(abbreviation, book, chapter)
chapter_data = self.__chapters_cache[cache_key]
verse_info = [v for v in chapter_data.get("verses", []) if str(v.get("verse")) == str(verse)]
if not verse_info:
raise ValueError(f"Verse {verse} not found in book {book}, chapter {chapter}.")
if cache_key in result:
existing_verses = {str(v["verse"]) for v in result[cache_key].get("verses", [])}
new_verses = [v for v in verse_info if str(v["verse"]) not in existing_verses]
result[cache_key]["verses"].extend(new_verses)
chapter_data = self.__retrieve_chapter_data(abbreviation, book, 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:
verse_data = chapter_data.copy()
verse_data["verses"] = verse_info
result[cache_key] = verse_data
chapter_data = self.__chapters_cache[cache_key]
for verse in 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}.")
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)
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]["verses"] = [verse_info]
def __check_translation(self, abbreviation):
"""

View File

@ -66,6 +66,74 @@ class TestGetBible(unittest.TestCase):
"text": "Wat van die begin af was, wat ons gehoor het, wat ons met ons o\u00eb gesien het, wat ons aanskou het en ons hande getas het aangaande die Woord van die lewe \u2014 "}]}}
self.assertEqual(actual_result, expected_result, "Failed to find 'Ge1:1;Jn1:1;1Jn1:1' scripture.")
def test_valid_multiple_reference_select_aleppo(self):
actual_result = self.getbible.select('Ge1:1-3;Ps1:1;ps1:1-2;Ge1:6-7,10', 'aleppo')
expected_result = {
'aleppo_19_1': {'abbreviation': 'aleppo',
'book_name': 'תְּהִלִּים',
'book_nr': 19,
'chapter': 1,
'direction': 'RTL',
'encoding': 'UTF-8',
'lang': 'hbo',
'language': 'Hebrew',
'name': 'תְּהִלִּים 1',
'translation': 'Aleppo Codex',
'verses': [{'chapter': 1,
'name': 'תְּהִלִּים 1:1',
'text': '\xa0\xa0אשרי האיש— \xa0\xa0 אשר לא הלך '
'בעצת רשעיםובדרך חטאים לא עמד \xa0\xa0 '
'ובמושב לצים לא ישב ',
'verse': 1},
{'chapter': 1,
'name': 'תְּהִלִּים 1:2',
'text': '\xa0\xa0כי אם בתורת יהוה חפצו \xa0\xa0 '
'ובתורתו יהגה יומם ולילה ',
'verse': 2}]},
'aleppo_1_1': {'abbreviation': 'aleppo',
'book_name': 'בְּרֵאשִׁית',
'book_nr': 1,
'chapter': 1,
'direction': 'RTL',
'encoding': 'UTF-8',
'lang': 'hbo',
'language': 'Hebrew',
'name': 'בְּרֵאשִׁית 1',
'translation': 'Aleppo Codex',
'verses': [{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:1',
'text': 'בראשית ברא אלהים את השמים ואת הארץ ',
'verse': 1},
{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:2',
'text': 'והארץ היתה תהו ובהו וחשך על פני תהום ורוח '
'אלהים מרחפת על פני המים ',
'verse': 2},
{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:3',
'text': 'ויאמר אלהים יהי אור ויהי אור ',
'verse': 3},
{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:6',
'text': 'ויאמר אלהים יהי רקיע בתוך המים ויהי מבדיל '
'בין מים למים ',
'verse': 6},
{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:7',
'text': 'ויעש אלהים את הרקיע ויבדל בין המים אשר '
'מתחת לרקיע ובין המים אשר מעל לרקיע ויהי '
'כן ',
'verse': 7},
{'chapter': 1,
'name': 'בְּרֵאשִׁית 1:10',
'text': 'ויקרא אלהים ליבשה ארץ ולמקוה המים קרא '
'ימים וירא אלהים כי טוב ',
'verse': 10}]
}
}
self.assertEqual(actual_result, expected_result, "Failed to find 'Ge1:1-3;Ps1:1;ps1:1-2;Ge1:6-7,1' scripture.")
if __name__ == '__main__':
unittest.main()