Files
kjvstudy.org/kjvstudy_org/utils/helpers.py
T
kennethreitz 115acd784c Fix critical Python code issues: bare except and unclosed file handles
- Replace bare except with specific exceptions in helpers.py
- Use context managers for all JSON file operations in about.py and api.py
  to ensure proper file handle cleanup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 10:40:30 -05:00

483 lines
23 KiB
Python

"""Helper utilities for KJV Study."""
import re
import json
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, List
from functools import lru_cache
from ..kjv import bible, VerseReference
from ..topics import get_all_topics
from ..red_letter import get_christ_words
from ..stories import get_all_stories_flat
from .books import normalize_book_name
# Paths to data files
_DATA_DIR = Path(__file__).parent.parent / "data"
_CHAPTER_EXPLANATIONS_PATH = _DATA_DIR / "chapter_explanations.json"
_POPULAR_CHAPTERS_PATH = _DATA_DIR / "popular_chapters.json"
_FEATURED_VERSES_PATH = _DATA_DIR / "featured_verses.json"
@lru_cache(maxsize=1)
def _load_chapter_explanations() -> dict:
"""Load chapter explanations from JSON file. Cached since data never changes."""
with open(_CHAPTER_EXPLANATIONS_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys to integers for chapter numbers
return {
book: {int(chapter): explanation for chapter, explanation in chapters.items()}
for book, chapters in data.items()
}
@lru_cache(maxsize=1)
def _load_popular_chapters() -> dict:
"""Load popular chapters from JSON file. Cached since data never changes."""
with open(_POPULAR_CHAPTERS_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys to integers for chapter numbers
popular = {
book: {int(chapter): score for chapter, score in chapters.items()}
for book, chapters in data["popular_chapters"].items()
}
return {
"popular_chapters": popular,
"high_readership_books": data["high_readership_books"]
}
@lru_cache(maxsize=1)
def _load_featured_verses() -> list:
"""Load featured verses from JSON file. Cached since data never changes."""
with open(_FEATURED_VERSES_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert dict format to tuple format for compatibility
return [
(verse["book"], verse["chapter"], verse["verse"])
for verse in data["verses"]
]
@lru_cache(maxsize=512)
def create_slug(text: str) -> str:
"""
Convert text to URL-friendly slug.
Cached since same inputs generate same outputs and called frequently.
"""
slug = re.sub(r'[^\w\s-]', '', text.lower())
slug = re.sub(r'[-\s]+', '-', slug)
return slug.strip('-')
def get_verse_text(book: str, chapter: int, verse: int) -> str:
"""Get the actual text of a specific verse."""
try:
text = bible.get_verse_text(book, chapter, verse)
if text:
return text
return f"{book} {chapter}:{verse} text not found"
except (KeyError, ValueError, TypeError, AttributeError):
return f"{book} {chapter}:{verse}"
def is_verse_reference(query: str) -> bool:
"""Check if query looks like a verse reference."""
# Pattern for verse references like "John 3:16", "1 John 4:8", "Genesis 1:1", etc.
# Also supports space format like "Rev 22 20" (without colon)
colon_pattern = r'^(I{1,3}|1|2|3)?\s*[A-Za-z]+(\s+[A-Za-z]+)?\s+\d+:\d+$'
space_pattern = r'^(I{1,3}|1|2|3)?\s*[A-Za-z]+(\s+[A-Za-z]+)?\s+\d+\s+\d+$'
query = query.strip()
return bool(re.match(colon_pattern, query) or re.match(space_pattern, query))
def parse_verse_reference(query: str) -> Optional[Dict]:
"""Parse a verse reference string and return verse info if found."""
try:
cleaned_query = query.strip()
verse_ref = VerseReference.from_string(cleaned_query)
# Try to normalize the book name (handles abbreviations like "Rev" -> "Revelation")
book = verse_ref.book
normalized = normalize_book_name(book)
if normalized:
book = normalized
verse_text = bible.get_verse_text(book, verse_ref.chapter, verse_ref.verse)
if verse_text:
return {
"book": book,
"chapter": verse_ref.chapter,
"verse": verse_ref.verse,
"text": verse_text,
"reference": f"{book} {verse_ref.chapter}:{verse_ref.verse}",
"url": f"/book/{book}/chapter/{verse_ref.chapter}#verse-{verse_ref.verse}",
"score": 100.0,
"highlighted_text": verse_text
}
except Exception as e:
print(f"Error parsing verse reference '{query}': {e}")
# Try alternative book name formats (Roman numerals to numbers)
try:
alternative_query = query.strip()
alternative_query = re.sub(r'^I\s+', '1 ', alternative_query)
alternative_query = re.sub(r'^II\s+', '2 ', alternative_query)
alternative_query = re.sub(r'^III\s+', '3 ', alternative_query)
if alternative_query != query.strip():
verse_ref = VerseReference.from_string(alternative_query)
# Try to normalize the book name
book = verse_ref.book
normalized = normalize_book_name(book)
if normalized:
book = normalized
verse_text = bible.get_verse_text(book, verse_ref.chapter, verse_ref.verse)
if verse_text:
return {
"book": book,
"chapter": verse_ref.chapter,
"verse": verse_ref.verse,
"text": verse_text,
"reference": f"{book} {verse_ref.chapter}:{verse_ref.verse}",
"url": f"/book/{book}/chapter/{verse_ref.chapter}#verse-{verse_ref.verse}",
"score": 100.0,
"highlighted_text": verse_text
}
except Exception as e2:
print(f"Alternative parsing also failed for '{query}': {e2}")
return None
@lru_cache(maxsize=256)
def get_related_content(book: str, chapter: int = None, verse: int = None) -> Dict:
"""
Get related study guides, topics, and resources for a given passage.
Cached since this does expensive dictionary lookups and topic searches.
"""
related = {
"study_guides": [],
"topics": [],
"people": [],
"resources": [],
"stories": []
}
# Find related Bible stories based on verse references
all_stories = get_all_stories_flat()
for story in all_stories:
story_verses = story.get("verses", [])
for verse_ref in story_verses:
# Parse verse references like "Genesis 1:1-31" or "Genesis 2:1-3"
if verse_ref.startswith(book + " "):
# Extract chapter from reference
ref_part = verse_ref[len(book) + 1:] # e.g., "1:1-31" or "2:1-3"
if ":" in ref_part:
ref_chapter = ref_part.split(":")[0]
try:
ref_chapter_num = int(ref_chapter)
# If chapter matches (or no chapter specified), include this story
if chapter is None or ref_chapter_num == chapter:
story_entry = {
"name": story.get("title", ""),
"url": f"/stories/{story.get('slug', '')}",
"description": story.get("description", "")[:100] + "..." if len(story.get("description", "")) > 100 else story.get("description", "")
}
# Avoid duplicates
if story_entry not in related["stories"]:
related["stories"].append(story_entry)
break # Only add each story once per book/chapter match
except ValueError:
pass
# Map books to related people
book_people_map = {
"Genesis": [{"name": "Abraham", "url": "/family-tree/person/i60"}, {"name": "Jacob", "url": "/family-tree/person/i58"}],
"Exodus": [{"name": "Moses", "url": "/biblical-prophets/moses"}],
"1 Samuel": [{"name": "Samuel", "url": "/biblical-prophets"}],
"2 Samuel": [{"name": "David", "url": "/family-tree/person/i113"}],
"1 Kings": [{"name": "Elijah", "url": "/biblical-prophets/elijah"}],
"2 Kings": [{"name": "Elijah", "url": "/biblical-prophets/elijah"}, {"name": "Elisha", "url": "/biblical-prophets"}],
"Isaiah": [{"name": "Isaiah", "url": "/biblical-prophets/isaiah"}],
"Jeremiah": [{"name": "Jeremiah", "url": "/biblical-prophets/jeremiah"}],
"Ezekiel": [{"name": "Ezekiel", "url": "/biblical-prophets/ezekiel"}],
"Daniel": [{"name": "Daniel", "url": "/biblical-prophets/daniel"}],
"Jonah": [{"name": "Jonah", "url": "/biblical-prophets/jonah"}],
"Matthew": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}],
"Mark": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}],
"Luke": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}, {"name": "John the Baptist", "url": "/biblical-prophets"}],
"John": [{"name": "John", "url": "/the-twelve-apostles/john"}],
"Acts": [{"name": "Peter", "url": "/the-twelve-apostles/simon-peter"}, {"name": "Paul", "url": "/the-twelve-apostles"}],
"Ruth": [{"name": "Ruth", "url": "/women-of-the-bible/ruth"}],
"Esther": [{"name": "Esther", "url": "/women-of-the-bible/esther"}],
}
if book in book_people_map:
related["people"] = book_people_map[book]
# Map books/passages to special resources
if book in ["Exodus", "Leviticus", "Numbers", "Deuteronomy"]:
related["resources"].append({"name": "Biblical Festivals", "url": "/biblical-festivals"})
related["resources"].append({"name": "Biblical Covenants", "url": "/biblical-covenants"})
if book in ["Genesis", "Exodus", "Numbers"]:
related["resources"].append({"name": "Biblical Timeline", "url": "/biblical-timeline"})
if book in ["Joshua", "Judges", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings"]:
related["resources"].append({"name": "Biblical Maps", "url": "/biblical-maps"})
if book in ["Matthew", "Mark", "Luke", "John"]:
related["resources"].append({"name": "Parables of Jesus", "url": "/parables"})
# Add topic links based on verse references in topic data
topics_data = get_all_topics()
for topic_name, topic_data in topics_data.items():
topic_matched = False
# Check subtopics for verse references
subtopics = topic_data.get("subtopics", {})
for subtopic_name, subtopic_data in subtopics.items():
if topic_matched:
break
verses = subtopic_data.get("verses", [])
for verse_entry in verses:
ref = verse_entry.get("ref", "")
# Check if this reference matches our book/chapter
if ref.startswith(book + " "):
ref_part = ref[len(book) + 1:]
if ":" in ref_part:
ref_chapter = ref_part.split(":")[0]
try:
ref_chapter_num = int(ref_chapter)
if chapter is None or ref_chapter_num == chapter:
related["topics"].append({
"name": topic_name,
"url": f"/topics/{topic_name}"
})
topic_matched = True
break
except ValueError:
pass
# Add specific resource pages based on book/chapter
specific_resources = {
# Sermon on the Mount / Beatitudes
("Matthew", 5): [{"name": "The Beatitudes", "url": "/beatitudes"}],
("Matthew", 6): [{"name": "Prayers of the Bible", "url": "/prayers-of-the-bible"}],
("Matthew", 7): [{"name": "The Beatitudes", "url": "/beatitudes"}],
# Ten Commandments
("Exodus", 20): [{"name": "Ten Commandments", "url": "/ten-commandments"}],
("Deuteronomy", 5): [{"name": "Ten Commandments", "url": "/ten-commandments"}],
# Armor of God & Spiritual Warfare
("Ephesians", 6): [{"name": "Armor of God", "url": "/armor-of-god"}, {"name": "Spirits and Demons", "url": "/spirits-and-demons"}],
# Fruits of the Spirit
("Galatians", 5): [{"name": "Fruits of the Spirit", "url": "/fruits-of-the-spirit"}],
# I Am Statements (John's Gospel)
("John", 6): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}],
("John", 8): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}],
("John", 10): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}],
("John", 11): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}],
("John", 14): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}, {"name": "The Trinity", "url": "/trinity"}],
("John", 15): [{"name": "I Am Statements of Jesus", "url": "/i-am-statements"}],
# John 1 - Christology, Names of Christ
("John", 1): [{"name": "Names of Christ", "url": "/names-of-christ"}, {"name": "Christology", "url": "/christology"}],
# Love chapter
("1 Corinthians", 13): [{"name": "Biblical Love", "url": "/topics/Love"}],
# Faith chapter
("Hebrews", 11): [{"name": "Heroes of Faith", "url": "/topics/Faith"}],
# Messianic prophecies in key OT chapters
("Isaiah", 7): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Isaiah", 9): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Isaiah", 11): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Isaiah", 53): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Micah", 5): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Zechariah", 9): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Zechariah", 12): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Psalm", 22): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Psalms", 22): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Psalms", 110): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Daniel", 7): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
("Daniel", 9): [{"name": "Messianic Prophecies", "url": "/messianic-prophecies"}],
# Names of God
("Exodus", 3): [{"name": "Names of God", "url": "/names-of-god"}, {"name": "Tetragrammaton", "url": "/tetragrammaton"}],
("Exodus", 6): [{"name": "Names of God", "url": "/names-of-god"}],
("Genesis", 14): [{"name": "Names of God", "url": "/names-of-god"}],
("Genesis", 22): [{"name": "Names of God", "url": "/names-of-god"}],
# Biblical covenants
("Genesis", 9): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("Genesis", 15): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("Genesis", 17): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("2 Samuel", 7): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("Jeremiah", 31): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("Hebrews", 8): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}],
("Hebrews", 9): [{"name": "Biblical Covenants", "url": "/biblical-covenants"}, {"name": "Blood in Scripture", "url": "/blood-in-scripture"}],
# Angels
("Genesis", 18): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Genesis", 19): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Daniel", 10): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Luke", 1): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Luke", 2): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Revelation", 4): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
("Revelation", 5): [{"name": "Biblical Angels", "url": "/biblical-angels"}],
# Trinity
("Matthew", 3): [{"name": "The Trinity", "url": "/trinity"}],
("Matthew", 28): [{"name": "The Trinity", "url": "/trinity"}],
("2 Corinthians", 13): [{"name": "The Trinity", "url": "/trinity"}],
# Holy Spirit / Pneumatology
("Acts", 2): [{"name": "Pneumatology", "url": "/pneumatology"}],
("John", 16): [{"name": "Pneumatology", "url": "/pneumatology"}],
("Romans", 8): [{"name": "Pneumatology", "url": "/pneumatology"}, {"name": "Providence", "url": "/providence"}],
# Eschatology
("Matthew", 24): [{"name": "Eschatology", "url": "/eschatology"}],
("Matthew", 25): [{"name": "Eschatology", "url": "/eschatology"}],
("1 Thessalonians", 4): [{"name": "Eschatology", "url": "/eschatology"}],
("2 Thessalonians", 2): [{"name": "Eschatology", "url": "/eschatology"}],
("Revelation", 20): [{"name": "Eschatology", "url": "/eschatology"}],
("Revelation", 21): [{"name": "Eschatology", "url": "/eschatology"}],
("Revelation", 22): [{"name": "Eschatology", "url": "/eschatology"}],
# Ecclesiology (Church)
("Matthew", 16): [{"name": "Ecclesiology", "url": "/ecclesiology"}],
("Matthew", 18): [{"name": "Ecclesiology", "url": "/ecclesiology"}],
("1 Corinthians", 12): [{"name": "Ecclesiology", "url": "/ecclesiology"}],
("Ephesians", 4): [{"name": "Ecclesiology", "url": "/ecclesiology"}],
# Soteriology (Salvation)
("Romans", 3): [{"name": "Soteriology", "url": "/soteriology"}],
("Romans", 5): [{"name": "Soteriology", "url": "/soteriology"}, {"name": "Justification", "url": "/justification"}],
("Romans", 6): [{"name": "Soteriology", "url": "/soteriology"}, {"name": "Sanctification", "url": "/sanctification"}],
("Ephesians", 2): [{"name": "Soteriology", "url": "/soteriology"}, {"name": "Grace", "url": "/grace"}],
("Titus", 3): [{"name": "Soteriology", "url": "/soteriology"}],
# Types and Shadows
("Hebrews", 10): [{"name": "Types and Shadows", "url": "/types-and-shadows"}],
# Blood in Scripture
("Leviticus", 17): [{"name": "Blood in Scripture", "url": "/blood-in-scripture"}],
# Kingdom of God
("Matthew", 13): [{"name": "Kingdom of God", "url": "/kingdom-of-god"}],
("Mark", 4): [{"name": "Kingdom of God", "url": "/kingdom-of-god"}],
("Luke", 17): [{"name": "Kingdom of God", "url": "/kingdom-of-god"}],
# Miracles of Jesus
("Matthew", 8): [{"name": "Miracles of Jesus", "url": "/miracles-of-jesus"}],
("Matthew", 9): [{"name": "Miracles of Jesus", "url": "/miracles-of-jesus"}],
("Mark", 5): [{"name": "Miracles of Jesus", "url": "/miracles-of-jesus"}],
("John", 2): [{"name": "Miracles of Jesus", "url": "/miracles-of-jesus"}],
("John", 9): [{"name": "Miracles of Jesus", "url": "/miracles-of-jesus"}],
# Christology
("Colossians", 1): [{"name": "Christology", "url": "/christology"}],
("Philippians", 2): [{"name": "Christology", "url": "/christology"}],
("Hebrews", 1): [{"name": "Christology", "url": "/christology"}],
# Law and Gospel
("Romans", 7): [{"name": "Law and Gospel", "url": "/law-and-gospel"}],
("Galatians", 3): [{"name": "Law and Gospel", "url": "/law-and-gospel"}],
# Hamartiology (Sin)
("Genesis", 3): [{"name": "Hamartiology", "url": "/hamartiology"}],
("Romans", 1): [{"name": "Hamartiology", "url": "/hamartiology"}],
# Providence
("Genesis", 50): [{"name": "Providence", "url": "/providence"}],
# Worship
("Psalms", 100): [{"name": "Worship", "url": "/worship"}],
("Psalms", 150): [{"name": "Worship", "url": "/worship"}],
("John", 4): [{"name": "Worship", "url": "/worship"}],
# Spirits and Demons
("1 Peter", 5): [{"name": "Spirits and Demons", "url": "/spirits-and-demons"}],
}
if chapter is not None:
key = (book, chapter)
if key in specific_resources:
for resource in specific_resources[key]:
if resource not in related["resources"]:
related["resources"].append(resource)
return related
# Load popular chapters data from JSON
_popular_data = _load_popular_chapters()
POPULAR_CHAPTERS = _popular_data["popular_chapters"]
HIGH_READERSHIP_BOOKS = _popular_data["high_readership_books"]
@lru_cache(maxsize=512)
def get_chapter_popularity_score(book: str, chapter: int) -> int:
"""
Calculate popularity score for a chapter (1-10 scale) based on well-known verses.
Cached since calculation is deterministic and called frequently.
"""
if book in POPULAR_CHAPTERS and chapter in POPULAR_CHAPTERS[book]:
return POPULAR_CHAPTERS[book][chapter]
default_score = 4
if chapter == 1:
default_score += 1
if book in HIGH_READERSHIP_BOOKS:
default_score += 1
total_chapters = len(bible.get_chapters_for_book(book))
if total_chapters <= 5:
default_score += 1
return min(default_score, 6)
# Load chapter explanations from JSON
CHAPTER_EXPLANATIONS = _load_chapter_explanations()
def get_chapter_popularity_explanation(book: str, chapter: int) -> str:
"""Get explanation for why a chapter is popular or what it contains."""
if book in CHAPTER_EXPLANATIONS and chapter in CHAPTER_EXPLANATIONS[book]:
return CHAPTER_EXPLANATIONS[book][chapter]
if chapter == 1:
return f"Opening chapter of {book} - introduces key themes and characters"
if book in ["Matthew", "Mark", "Luke", "John"]:
return "Gospel account of Jesus' life and ministry"
elif book in ["Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy"]:
return "Torah/Pentateuch - foundational law and history of Israel"
elif book in ["Psalms", "Proverbs", "Ecclesiastes", "Song of Solomon"]:
return "Wisdom literature - poetry and practical life guidance"
elif book in ["Isaiah", "Jeremiah", "Ezekiel", "Daniel"]:
return "Major prophet - messages of judgment and hope"
elif book in ["Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians",
"Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians",
"1 Timothy", "2 Timothy", "Titus", "Philemon"]:
return "Pauline epistle - apostolic teaching for the early church"
elif book == "Acts":
return "History of the early church and spread of the gospel"
elif book == "Revelation":
return "Apocalyptic vision of the end times and Christ's victory"
else:
return f"Part of {book} - explore this chapter to discover its significance"
# Load featured verses from JSON
FEATURED_VERSES = _load_featured_verses()
def get_daily_verse() -> Dict:
"""Get the verse of the day based on the current date."""
today = datetime.now()
day_of_year = today.timetuple().tm_yday
verse_index = day_of_year % len(FEATURED_VERSES)
book, chapter, verse = FEATURED_VERSES[verse_index]
verse_text = bible.get_verse_text(book, chapter, verse)
christ_words = get_christ_words(book, chapter, verse)
return {
"book": book,
"chapter": chapter,
"verse": verse,
"text": verse_text,
"reference": f"{book} {chapter}:{verse}",
"url": f"/book/{book}/chapter/{chapter}#verse-{verse}",
"red_letter": christ_words
}