Files
kjvstudy.org/kjvstudy_org/routes/api.py
T
kennethreitz d24903a8dc Add specific resource pages to related content and fix reading plan API
- Expand related resources with specific chapter mappings (Beatitudes, Armor of God,
  I Am Statements, Trinity, Eschatology, Soteriology, etc.)
- Click on verse text navigates to verse page (mouse users), green selection is
  keyboard-only
- Fix reading plan API import error (get_reading_text from reading_plans route)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 18:24:46 -05:00

2561 lines
98 KiB
Python

"""API routes for KJV Study - JSON endpoints for programmatic access."""
from typing import Optional, List
from pydantic import BaseModel, Field
import json
import random
import re
from pathlib import Path as FilePath
from functools import lru_cache
from fastapi import APIRouter, HTTPException, Query, Path, Body
from fastapi.responses import JSONResponse, StreamingResponse
from ..utils.pdf import render_html_to_pdf, render_html_to_pdf_async, WEASYPRINT_AVAILABLE
from ..kjv import bible
from ..cross_references import get_cross_references
from ..reading_plans import get_plan, get_plan_summary
from ..topics import get_all_topics, get_topic_with_text
from ..interlinear_loader import get_interlinear_data, has_interlinear_data
from ..utils.books import normalize_book_name, OT_BOOKS, NT_BOOKS
from ..utils.search import perform_full_text_search
from ..utils.helpers import get_daily_verse, create_slug, CHAPTER_EXPLANATIONS
from ..utils.commentary_loader import load_commentary
from ..books import get_book_data, get_all_books_metadata, has_book_data
from ..red_letter import get_christ_words, load_red_letter_verses
from ..strongs import (
get_strongs_entry, format_strongs_entry, search_strongs,
get_strongs_definition, get_strongs_word
)
from ..stories import (
get_categories,
get_story_by_slug,
get_story_count,
get_category_count,
get_all_stories_flat,
)
from ..data import (
_data as RESOURCES_DATA,
find_resource_by_slug,
_create_slug,
# Import all specific resource dicts
BIBLICAL_LOCATIONS, ANGELS_DATA, PROPHETS_DATA, NAMES_DATA,
PARABLES_DATA, COVENANTS_DATA, APOSTLES_DATA, WOMEN_DATA,
FESTIVALS_DATA, FRUITS_DATA, MIRACLES_DATA, PRAYERS_DATA,
BEATITUDES_DATA, TEN_COMMANDMENTS_DATA, ARMOR_OF_GOD_DATA,
I_AM_STATEMENTS_DATA, TRINITY_DATA, CHRISTOLOGY_DATA,
SOTERIOLOGY_DATA, PNEUMATOLOGY_DATA, ESCHATOLOGY_DATA,
ECCLESIOLOGY_DATA, TYPES_AND_SHADOWS_DATA, MESSIANIC_PROPHECIES_DATA,
BLOOD_IN_SCRIPTURE_DATA, KINGDOM_OF_GOD_DATA, NAMES_OF_CHRIST_DATA,
SPIRITS_AND_DEMONS_DATA, PERSONIFICATIONS_DATA, BIBLIOLOGY_DATA,
THEOLOGY_PROPER_DATA, ANTHROPOLOGY_DATA, HAMARTIOLOGY_DATA,
PROVIDENCE_DATA, GRACE_DATA, JUSTIFICATION_DATA, SANCTIFICATION_DATA,
LAW_AND_GOSPEL_DATA, WORSHIP_DATA
)
router = APIRouter(prefix="/api", tags=["API"])
# Templates will be set by the main app
templates = None
@lru_cache(maxsize=1)
def _load_verse_commentary():
"""Load verse commentary from split per-book files. Cached since data never changes."""
return load_commentary()
@lru_cache(maxsize=1)
def _load_biographies():
"""Load biographies from JSON file. Cached since data never changes."""
biographies_path = FilePath(__file__).parent.parent / "data" / "biographies.json"
if not biographies_path.exists():
return {"biographies": {}, "aliases": {}}
with open(biographies_path, "r", encoding="utf-8") as f:
return json.load(f)
@lru_cache(maxsize=1)
def _load_close_family_marriages():
"""Load known close family marriages from JSON file. Cached since data never changes."""
marriages_path = FilePath(__file__).parent.parent / "data" / "close_family_marriages.json"
if not marriages_path.exists():
return {"marriages": []}
with open(marriages_path, "r", encoding="utf-8") as f:
return json.load(f)
# Pydantic models for API responses
class VerseResponse(BaseModel):
"""Response model for a single verse"""
book: str = Field(..., json_schema_extra={"example": "John"})
chapter: int = Field(..., json_schema_extra={"example": 3})
verse: int = Field(..., json_schema_extra={"example": 16})
reference: str = Field(..., json_schema_extra={"example": "John 3:16"})
text: str = Field(..., json_schema_extra={"example": "For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life."})
red_letter: Optional[str] = Field(
None,
description="Words of Christ: null if Jesus doesn't speak, 'full' if entire verse, or the quoted words if partial",
json_schema_extra={"example": "full"}
)
class VerseInRange(BaseModel):
"""Single verse within a range"""
verse: int = Field(..., json_schema_extra={"example": 1})
text: str = Field(..., json_schema_extra={"example": "The LORD is my shepherd; I shall not want."})
red_letter: Optional[str] = Field(
None,
description="Words of Christ: null if Jesus doesn't speak, 'full' if entire verse, or the quoted words if partial"
)
class VerseRangeResponse(BaseModel):
"""Response model for a range of verses"""
book: str = Field(..., json_schema_extra={"example": "Psalms"})
chapter: int = Field(..., json_schema_extra={"example": 23})
start: int = Field(..., json_schema_extra={"example": 1})
end: int = Field(..., json_schema_extra={"example": 6})
reference: str = Field(..., json_schema_extra={"example": "Psalms 23:1-6"})
verses: List[VerseInRange]
text: str = Field(..., json_schema_extra={"example": "The LORD is my shepherd; I shall not want..."})
class DailyVerseResponse(BaseModel):
"""Response model for verse of the day"""
book: str = Field(..., json_schema_extra={"example": "John"})
chapter: int = Field(..., json_schema_extra={"example": 3})
verse: int = Field(..., json_schema_extra={"example": 16})
text: str = Field(..., json_schema_extra={"example": "For God so loved the world..."})
reference: str = Field(..., json_schema_extra={"example": "John 3:16"})
url: str = Field(..., json_schema_extra={"example": "/book/John/chapter/3#verse-16"})
red_letter: Optional[str] = Field(
None,
description="Words of Christ: null if Jesus doesn't speak, 'full' if entire verse, or the quoted words if partial",
json_schema_extra={"example": "full"}
)
class ResourceVerse(BaseModel):
"""Verse reference in a resource"""
reference: str = Field(..., json_schema_extra={"example": "Genesis 2:8"})
text: str = Field(..., json_schema_extra={"example": "And the LORD God planted a garden..."})
class ResourceCategoryInfo(BaseModel):
"""Information about a resource category"""
name: str = Field(..., json_schema_extra={"example": "biblical_locations"})
title: str = Field(..., json_schema_extra={"example": "Biblical Locations"})
item_count: int = Field(..., json_schema_extra={"example": 15})
url: str = Field(..., json_schema_extra={"example": "/api/resources/biblical_locations"})
html_url: str = Field(..., json_schema_extra={"example": "/biblical-locations"})
class ResourcesListResponse(BaseModel):
"""Response listing all resource categories"""
total_categories: int = Field(..., json_schema_extra={"example": 39})
categories: List[ResourceCategoryInfo]
class ResourceItemSummary(BaseModel):
"""Summary of a resource item"""
name: str = Field(..., json_schema_extra={"example": "Garden of Eden"})
slug: str = Field(..., json_schema_extra={"example": "garden-of-eden"})
description: str = Field(..., json_schema_extra={"example": "The original home of mankind"})
verse_count: int = Field(..., json_schema_extra={"example": 2})
url: str = Field(..., json_schema_extra={"example": "/api/resources/biblical_locations/garden-of-eden"})
class ResourceCategoryResponse(BaseModel):
"""Response for a specific resource category"""
category: str = Field(..., json_schema_extra={"example": "biblical_locations"})
title: str = Field(..., json_schema_extra={"example": "Biblical Locations"})
total_items: int = Field(..., json_schema_extra={"example": 15})
items: List[ResourceItemSummary]
class ResourceItemDetail(BaseModel):
"""Detailed information about a specific resource item"""
name: str = Field(..., json_schema_extra={"example": "Garden of Eden"})
slug: str = Field(..., json_schema_extra={"example": "garden-of-eden"})
category: str = Field(..., json_schema_extra={"example": "biblical_locations"})
description: str = Field(..., json_schema_extra={"example": "The original home of mankind"})
verses: List[ResourceVerse]
class RedLetterVerse(BaseModel):
"""A verse containing words of Christ"""
reference: str = Field(..., json_schema_extra={"example": "John 3:16"})
book: str = Field(..., json_schema_extra={"example": "John"})
chapter: int = Field(..., json_schema_extra={"example": 3})
verse: int = Field(..., json_schema_extra={"example": 16})
text: str = Field(..., json_schema_extra={"example": "For God so loved the world..."})
christ_words: str = Field(..., json_schema_extra={"example": "full"})
is_full_verse: bool = Field(..., json_schema_extra={"example": True})
class RedLetterListResponse(BaseModel):
"""Response listing red letter verses"""
total: int = Field(..., json_schema_extra={"example": 1842})
verses: List[RedLetterVerse]
limit: int = Field(..., json_schema_extra={"example": 50})
offset: int = Field(..., json_schema_extra={"example": 0})
class RedLetterStatsResponse(BaseModel):
"""Statistics about red letter verses"""
total_verses: int = Field(..., json_schema_extra={"example": 1842})
full_verses: int = Field(..., json_schema_extra={"example": 1456})
partial_verses: int = Field(..., json_schema_extra={"example": 386})
books_with_red_letter: List[str] = Field(..., json_schema_extra={"example": ["Matthew", "Mark", "Luke", "John"]})
by_book: dict = Field(..., json_schema_extra={"example": {"Matthew": 644, "Mark": 285}})
class CommentaryResponse(BaseModel):
"""Verse commentary response"""
book: str = Field(..., json_schema_extra={"example": "John"})
chapter: int = Field(..., json_schema_extra={"example": 3})
verse: int = Field(..., json_schema_extra={"example": 16})
reference: str = Field(..., json_schema_extra={"example": "John 3:16"})
text: str = Field(..., json_schema_extra={"example": "For God so loved the world..."})
analysis: str = Field(..., json_schema_extra={"example": "<strong>For I am the LORD...</strong>"})
historical: str = Field(..., json_schema_extra={"example": "Historical context..."})
questions: List[str] = Field(..., json_schema_extra={"example": ["How does this verse...", "What does..."]})
class ChapterCommentaryResponse(BaseModel):
"""Chapter commentary response"""
book: str = Field(..., json_schema_extra={"example": "Genesis"})
chapter: int = Field(..., json_schema_extra={"example": 1})
explanation: str = Field(..., json_schema_extra={"example": "The creation account..."})
class BulkVerseRequest(BaseModel):
"""Request body for bulk verse lookup"""
references: List[str] = Field(..., json_schema_extra={"example": ["John 3:16", "Romans 8:28", "Psalm 23:1"]})
class BulkVerseResponse(BaseModel):
"""Response for bulk verse lookup"""
total: int = Field(..., json_schema_extra={"example": 3})
verses: List[VerseResponse]
class KeyEvent(BaseModel):
"""A key event in a person's life"""
age: int = Field(..., json_schema_extra={"example": 100})
event: str = Field(..., json_schema_extra={"example": "Birth of Isaac"})
verse: str = Field(..., json_schema_extra={"example": "Genesis 21:5"})
class BiographyResponse(BaseModel):
"""Biography of a biblical figure"""
name: str = Field(..., json_schema_extra={"example": "Abraham"})
summary: str = Field(..., json_schema_extra={"example": "Originally named Abram..."})
significance: str = Field(..., json_schema_extra={"example": "Abraham is the father of the Hebrew nation..."})
key_events: List[KeyEvent]
class FamilyTreeListResponse(BaseModel):
"""List of all people in family tree"""
total: int = Field(..., json_schema_extra={"example": 42})
people: List[str] = Field(..., json_schema_extra={"example": ["Adam", "Noah", "Abraham"]})
class PersonStat(BaseModel):
"""Statistical information about a person"""
name: str = Field(..., json_schema_extra={"example": "Methuselah"})
person_id: str = Field(..., json_schema_extra={"example": "i12"})
value: int = Field(..., json_schema_extra={"example": 969})
additional_info: Optional[str] = Field(None, json_schema_extra={"example": "Lived 969 years"})
class FamilyTreeStatsResponse(BaseModel):
"""Statistics about the biblical family tree from GEDCOM data"""
total_people: int = Field(..., json_schema_extra={"example": 429})
total_generations: int = Field(..., json_schema_extra={"example": 77})
longest_lived: PersonStat
most_children: PersonStat
most_siblings: PersonStat
average_lifespan: Optional[float] = Field(None, json_schema_extra={"example": 256.5})
total_with_known_ages: int = Field(..., json_schema_extra={"example": 156})
close_family_marriages: int = Field(..., json_schema_extra={"example": 3}, description="Marriages between close relatives (common in early biblical times)")
# Mapping of category names to their data dictionaries
CATEGORY_TO_DATA = {
'biblical_locations': BIBLICAL_LOCATIONS,
'angels': ANGELS_DATA,
'prophets': PROPHETS_DATA,
'names': NAMES_DATA,
'parables': PARABLES_DATA,
'covenants': COVENANTS_DATA,
'apostles': APOSTLES_DATA,
'women': WOMEN_DATA,
'festivals': FESTIVALS_DATA,
'fruits': FRUITS_DATA,
'miracles': MIRACLES_DATA,
'prayers': PRAYERS_DATA,
'beatitudes': BEATITUDES_DATA,
'ten_commandments': TEN_COMMANDMENTS_DATA,
'armor_of_god': ARMOR_OF_GOD_DATA,
'i_am_statements': I_AM_STATEMENTS_DATA,
'trinity': TRINITY_DATA,
'christology': CHRISTOLOGY_DATA,
'soteriology': SOTERIOLOGY_DATA,
'pneumatology': PNEUMATOLOGY_DATA,
'eschatology': ESCHATOLOGY_DATA,
'ecclesiology': ECCLESIOLOGY_DATA,
'types_and_shadows': TYPES_AND_SHADOWS_DATA,
'messianic_prophecies': MESSIANIC_PROPHECIES_DATA,
'blood_in_scripture': BLOOD_IN_SCRIPTURE_DATA,
'kingdom_of_god': KINGDOM_OF_GOD_DATA,
'names_of_christ': NAMES_OF_CHRIST_DATA,
'spirits_and_demons': SPIRITS_AND_DEMONS_DATA,
'personifications': PERSONIFICATIONS_DATA,
'bibliology': BIBLIOLOGY_DATA,
'theology_proper': THEOLOGY_PROPER_DATA,
'anthropology': ANTHROPOLOGY_DATA,
'hamartiology': HAMARTIOLOGY_DATA,
'providence': PROVIDENCE_DATA,
'grace': GRACE_DATA,
'justification': JUSTIFICATION_DATA,
'sanctification': SANCTIFICATION_DATA,
'law_and_gospel': LAW_AND_GOSPEL_DATA,
'worship': WORSHIP_DATA
}
def init_templates(app_templates):
"""Initialize templates from the main app."""
global templates
templates = app_templates
@router.get("/")
async def api_index():
"""API index with links to documentation and available endpoints."""
return {
"name": "KJV Study API",
"version": "1.0.0",
"description": "RESTful API for accessing King James Bible verses and study resources",
"documentation": {
"swagger_ui": "/api/docs",
"redoc": "/api/redoc",
"openapi_json": "/api/openapi.json"
},
"endpoints": {
"health": "/api/health",
"search": "/api/search?q={query}",
"verse_of_the_day": "/api/verse-of-the-day",
"verse": "/api/verse/{book}/{chapter}/{verse}",
"verse_range": "/api/verse-range/{book}/{chapter}/{start}/{end}",
"interlinear": "/api/interlinear/{book}/{chapter}/{verse}",
"books": "/api/books",
"book": "/api/books/{book}",
"chapter": "/api/books/{book}/chapters/{chapter}",
"book_text": "/api/books/{book}/text",
"bible": "/api/bible",
"cross_references": "/api/cross-references/{book}/{chapter}/{verse}",
"topics": "/api/topics",
"topic": "/api/topics/{topic_name}",
"reading_plans": "/api/reading-plans",
"reading_plan": "/api/reading-plans/{plan_id}",
"stories": "/api/stories",
"story": "/api/stories/{slug}",
"red_letter": "/api/red-letter?book={book}&limit={limit}&offset={offset}",
"red_letter_stats": "/api/red-letter/stats",
"random_verse": "/api/verse/random?testament={testament}&book={book}",
"commentary": "/api/commentary/{book}/{chapter}/{verse}",
"chapter_commentary": "/api/chapter-commentary/{book}/{chapter}",
"bulk_verses": "/api/verses/bulk",
"family_tree": "/api/family-tree",
"family_tree_stats": "/api/family-tree/stats",
"biography": "/api/family-tree/{name}"
}
}
@router.get("/health")
async def api_health_check():
"""API health check endpoint for monitoring and status verification."""
return {
"status": "healthy",
"service": "KJV Study API",
"version": "1.0.0"
}
@router.get("/search")
async def search_api(
q: str = Query(..., description="Search query", example="faith"),
limit: Optional[int] = Query(None, description="Max results", example=10)
):
"""JSON API endpoint for search."""
if not q or len(q.strip()) < 2:
return {"query": q, "results": [], "total": 0}
results = perform_full_text_search(q.strip(), limit)
is_direct_verse = False
# Check if this was a direct verse reference match
if results and len(results) == 1 and results[0].get("score") == 100.0:
is_direct_verse = True
return {
"query": q,
"results": results,
"total": len(results),
"is_direct_verse": is_direct_verse
}
@router.get("/universal-search")
async def universal_search_api(
q: str = Query(..., description="Search query", example="love"),
limit: int = Query(5, description="Max results per category", example=5)
):
"""Universal search across all content types."""
if not q or len(q.strip()) < 2:
return {"query": q, "results": {}}
query = q.strip().lower()
results = {}
# Search Bible books (with common synonyms/misspellings)
book_synonyms = {
"song of songs": "Song of Solomon",
"canticles": "Song of Solomon",
"revelations": "Revelation",
"apocalypse": "Revelation",
"psalters": "Psalms",
"proverb": "Proverbs",
"ecclesiast": "Ecclesiastes",
"lament": "Lamentations",
"phil": "Philippians",
"1 sam": "1 Samuel",
"2 sam": "2 Samuel",
"1 kin": "1 Kings",
"2 kin": "2 Kings",
"1 chron": "1 Chronicles",
"2 chron": "2 Chronicles",
"1 cor": "1 Corinthians",
"2 cor": "2 Corinthians",
"1 thess": "1 Thessalonians",
"2 thess": "2 Thessalonians",
"1 tim": "1 Timothy",
"2 tim": "2 Timothy",
"1 pet": "1 Peter",
"2 pet": "2 Peter",
"1 joh": "1 John",
"2 joh": "2 John",
"3 joh": "3 John",
}
all_books = bible.get_books()
matching_books = []
# Check for synonym matches first
for synonym, book_name in book_synonyms.items():
if query in synonym and book_name not in [b["name"] for b in matching_books]:
matching_books.append({"name": book_name, "url": f"/book/{book_name}"})
# Then check direct book name matches
for book in all_books:
if query in book.lower() and book not in [b["name"] for b in matching_books]:
matching_books.append({"name": book, "url": f"/book/{book}"})
matching_books = matching_books[:limit]
if matching_books:
results["books"] = matching_books
# Search Bible verses (limit to top results for speed)
verse_results = perform_full_text_search(q.strip(), limit)
if verse_results:
results["verses"] = [
{
"reference": r["reference"],
"text": r["text"][:100] + "..." if len(r.get("text", "")) > 100 else r.get("text", ""),
"url": f"/book/{r['book']}/chapter/{r['chapter']}/verse/{r['verse']}"
}
for r in verse_results
]
# Search topics
all_topics = get_all_topics()
matching_topics = [
{"name": name.replace("_", " ").title(), "url": f"/topics/{name}"}
for name, data in all_topics.items()
if query in name.lower() or query in data.get("description", "").lower()
][:limit]
if matching_topics:
results["topics"] = matching_topics
# Search stories
all_stories = get_all_stories_flat()
matching_stories = [
{
"title": s["title"],
"url": f"/stories/{s['slug']}",
"category": s.get("category_name", "")
}
for s in all_stories
if query in s.get("title", "").lower() or query in s.get("description", "").lower()
][:limit]
if matching_stories:
results["stories"] = matching_stories
# Search reading plans
from ..reading_plans import READING_PLANS
matching_plans = [
{"name": plan["name"], "url": f"/reading-plans/{plan_id}"}
for plan_id, plan in READING_PLANS.items()
if query in plan["name"].lower() or query in plan.get("description", "").lower()
][:limit]
if matching_plans:
results["plans"] = matching_plans
# Search resources (theological studies, biblical figures, etc.)
resources_to_search = [
# Theological studies with synonyms
("trinity godhead three persons father son spirit", "/trinity", "The Trinity"),
("christology jesus christ lord savior messiah", "/christology", "Christology"),
("soteriology salvation saved redemption atonement", "/soteriology", "Soteriology"),
("pneumatology holy spirit ghost comforter paraclete", "/pneumatology", "Pneumatology"),
("eschatology end times last days rapture tribulation millennium", "/eschatology", "Eschatology"),
("ecclesiology church body believers congregation", "/ecclesiology", "Ecclesiology"),
("types_and_shadows typology foreshadow prefigure", "/types-and-shadows", "Types and Shadows"),
("messianic_prophecies prophecy predictions foretold", "/messianic-prophecies", "Messianic Prophecies"),
("blood_in_scripture sacrifice atonement covering", "/blood-in-scripture", "The Blood in Scripture"),
("kingdom_of_god reign throne rule", "/kingdom-of-god", "The Kingdom of God"),
("names_of_christ jesus titles lord savior", "/names-of-christ", "Names of Christ"),
("spirits_and_demons devils satan evil unclean", "/spirits-and-demons", "Spirits and Demons"),
("personifications wisdom folly death", "/personifications", "Personifications"),
("angels cherubim seraphim michael gabriel", "/biblical-angels", "Biblical Angels"),
("prophets elijah elisha isaiah jeremiah ezekiel daniel", "/biblical-prophets", "Biblical Prophets"),
("names_of_god yahweh jehovah el shaddai adonai", "/names-of-god", "Names of God"),
("parables stories teachings", "/parables", "Parables of Jesus"),
("covenants abrahamic mosaic davidic new", "/biblical-covenants", "Biblical Covenants"),
("apostles disciples twelve peter james john", "/the-twelve-apostles", "The Twelve Apostles"),
("women ruth esther mary martha rahab", "/women-of-the-bible", "Women of the Bible"),
("festivals passover pentecost tabernacles feast", "/biblical-festivals", "Biblical Festivals"),
("fruits love joy peace patience kindness goodness faithfulness gentleness self-control", "/fruits-of-the-spirit", "Fruits of the Spirit"),
("miracles healing signs wonders", "/miracles-of-jesus", "Miracles of Jesus"),
("prayers lord's prayer model", "/prayers-of-the-bible", "Prayers of the Bible"),
("beatitudes blessed sermon mount", "/beatitudes", "The Beatitudes"),
("ten_commandments decalogue law sinai", "/ten-commandments", "Ten Commandments"),
("armor_of_god warfare helmet breastplate shield sword", "/armor-of-god", "Armor of God"),
("i_am_statements bread light door shepherd resurrection way truth life vine", "/i-am-statements", "I Am Statements"),
("tetragrammaton yhwh yahweh jehovah lord", "/tetragrammaton", "The Tetragrammaton"),
("timeline chronology history dates", "/biblical-timeline", "Biblical Timeline"),
("family_tree genealogy lineage ancestors descendants", "/family-tree", "Biblical Genealogies"),
("interlinear hebrew greek original language", "/interlinear", "Interlinear Bible"),
("concordance word search find", "/concordance", "Concordance"),
("study_guides", "/study-guides", "Study Guides"),
("maps geography places locations", "/maps", "Bible Maps"),
# Individual study guides
("new believer faith basics beginner", "/study-guides/new-believer", "New Believer's Guide"),
("salvation saved born again", "/study-guides/salvation", "Understanding Salvation"),
("gospel good news", "/study-guides/gospel", "The Gospel Message"),
("fruits spirit love joy peace", "/study-guides/fruits-spirit", "Fruits of the Spirit Guide"),
("prayer faith praying", "/study-guides/prayer-faith", "Prayer & Faith"),
("christian living walk daily", "/study-guides/christian-living", "Christian Living"),
("god's love agape", "/study-guides/gods-love", "God's Love"),
("hope comfort suffering trials", "/study-guides/hope-comfort", "Hope & Comfort"),
("wisdom guidance direction decisions", "/study-guides/wisdom-guidance", "Wisdom & Guidance"),
("trinity father son spirit", "/study-guides/trinity", "Trinity Study Guide"),
("resurrection risen easter", "/study-guides/resurrection", "The Resurrection"),
("heaven eternity afterlife", "/study-guides/heaven-eternity", "Heaven & Eternity"),
("sovereignty of god control providence", "/study-guides/sovereignty-of-god", "Sovereignty of God"),
("attributes of god character nature", "/study-guides/attributes-of-god", "Attributes of God"),
("doctrine of scripture bible inspiration inerrancy", "/study-guides/doctrine-of-scripture", "Doctrine of Scripture"),
("problem of evil theodicy suffering why", "/study-guides/problem-of-evil", "Problem of Evil"),
("covenant theology dispensation", "/study-guides/covenant-theology", "Covenant Theology"),
("spirits demons spiritual warfare", "/study-guides/spirits-demons", "Spirits & Demons Guide"),
("gospel in old testament types shadows", "/study-guides/gospel-in-ot", "Gospel in the Old Testament"),
("law and christian grace mosaic", "/study-guides/law-and-christian", "The Law and the Christian"),
("faith and works james paul", "/study-guides/faith-and-works", "Faith and Works"),
("scarlet thread redemption blood", "/study-guides/scarlet-thread", "Scarlet Thread of Redemption"),
("biblical marriage husband wife", "/study-guides/biblical-marriage", "Biblical Marriage"),
("raising children parenting family", "/study-guides/raising-children", "Raising Children"),
("money stewardship finances tithe giving", "/study-guides/money-stewardship", "Money & Stewardship"),
# Biblical figures (link to family tree)
("adam eve first man woman creation", "/family-tree/adam", "Adam"),
("noah ark flood", "/family-tree/noah", "Noah"),
("abraham abram father faith", "/family-tree/abraham", "Abraham"),
("isaac son promise", "/family-tree/isaac", "Isaac"),
("jacob israel twelve tribes", "/family-tree/jacob", "Jacob"),
("joseph dreamer coat egypt", "/family-tree/joseph", "Joseph"),
("moses exodus law lawgiver", "/family-tree/moses", "Moses"),
("david king shepherd psalmist", "/family-tree/david", "David"),
("solomon wisdom temple", "/family-tree/solomon", "Solomon"),
("paul apostle gentiles saul tarsus", "/family-tree/paul", "Paul the Apostle"),
]
matching_resources = [
{"name": name, "url": url}
for key, url, name in resources_to_search
if query in key.lower() or query in name.lower()
][:limit]
if matching_resources:
results["resources"] = matching_resources
return {"query": q, "results": results}
@router.get(
"/verse-of-the-day",
response_model=DailyVerseResponse,
summary="Get verse of the day",
description="Returns a featured verse that changes daily"
)
async def verse_of_the_day_api():
"""API endpoint for verse of the day."""
return get_daily_verse()
@router.get(
"/verse/{book}/{chapter}/{verse}",
response_model=VerseResponse,
summary="Get a single verse",
description="Retrieve a specific Bible verse by book, chapter, and verse number. Includes red letter (words of Christ) information. Optionally include interlinear Hebrew/Greek data.",
responses={
200: {
"description": "Successfully retrieved verse",
"content": {
"application/json": {
"examples": {
"jesus_speaks": {
"summary": "Verse where Jesus speaks (full)",
"value": {
"book": "John",
"chapter": 3,
"verse": 16,
"reference": "John 3:16",
"text": "For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.",
"red_letter": "full"
}
},
"no_jesus": {
"summary": "Verse without Jesus speaking",
"value": {
"book": "Genesis",
"chapter": 1,
"verse": 1,
"reference": "Genesis 1:1",
"text": "In the beginning God created the heaven and the earth.",
"red_letter": None
}
},
"partial_jesus": {
"summary": "Verse where Jesus speaks part of it",
"value": {
"book": "Matthew",
"chapter": 4,
"verse": 4,
"reference": "Matthew 4:4",
"text": "But he answered and said, It is written, Man shall not live by bread alone, but by every word that proceedeth out of the mouth of God.",
"red_letter": "It is written, Man shall not live by bread alone, but by every word that proceedeth out of the mouth of God."
}
}
}
}
}
}
}
)
async def api_get_verse(
book: str = Path(..., description="Book name (supports abbreviations)", example="John"),
chapter: int = Path(..., description="Chapter number", example=3, ge=1),
verse: int = Path(..., description="Verse number", example=16, ge=1),
interlinear: bool = Query(False, description="Include interlinear Hebrew/Greek word-by-word data")
):
"""Get a single verse with red letter information and optional interlinear data."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Check if book exists
all_books = bible.get_books()
if book not in all_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
# Check valid chapter/verse numbers
if chapter < 1 or verse < 1:
raise HTTPException(status_code=404, detail="Invalid chapter or verse number")
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
raise HTTPException(status_code=404, detail="Verse not found")
# Get red letter information (words of Christ)
christ_words = get_christ_words(book, chapter, verse)
result = {
"book": book,
"chapter": chapter,
"verse": verse,
"reference": f"{book} {chapter}:{verse}",
"text": verse_text,
"red_letter": christ_words
}
# Optionally include interlinear data
if interlinear:
interlinear_words = get_interlinear_data(book, chapter, verse)
result["interlinear"] = {
"available": bool(interlinear_words),
"words": interlinear_words or []
}
return JSONResponse(result)
@router.get(
"/verse-range/{book}/{chapter}/{start}/{end}",
response_model=VerseRangeResponse,
summary="Get a range of verses",
description="Retrieve multiple consecutive verses from a chapter. Each verse includes red letter information.",
responses={
200: {
"description": "Successfully retrieved verse range",
"content": {
"application/json": {
"examples": {
"sermon_on_mount": {
"summary": "Beatitudes (Jesus speaking)",
"value": {
"book": "Matthew",
"chapter": 5,
"start": 3,
"end": 5,
"reference": "Matthew 5:3-5",
"verses": [
{
"verse": 3,
"text": "Blessed are the poor in spirit: for their's is the kingdom of heaven.",
"red_letter": "full"
},
{
"verse": 4,
"text": "Blessed are they that mourn: for they shall be comforted.",
"red_letter": "full"
},
{
"verse": 5,
"text": "Blessed are the meek: for they shall inherit the earth.",
"red_letter": "full"
}
],
"text": "Blessed are the poor in spirit: for their's is the kingdom of heaven. Blessed are they that mourn: for they shall be comforted. Blessed are the meek: for they shall inherit the earth."
}
},
"psalm": {
"summary": "Psalm 23 excerpt (no Jesus)",
"value": {
"book": "Psalms",
"chapter": 23,
"start": 1,
"end": 3,
"reference": "Psalms 23:1-3",
"verses": [
{
"verse": 1,
"text": "The LORD is my shepherd; I shall not want.",
"red_letter": None
},
{
"verse": 2,
"text": "He maketh me to lie down in green pastures: he leadeth me beside the still waters.",
"red_letter": None
},
{
"verse": 3,
"text": "He restoreth my soul: he leadeth me in the paths of righteousness for his name's sake.",
"red_letter": None
}
],
"text": "The LORD is my shepherd; I shall not want. He maketh me to lie down in green pastures: he leadeth me beside the still waters. He restoreth my soul: he leadeth me in the paths of righteousness for his name's sake."
}
}
}
}
}
}
}
)
async def api_get_verse_range(
book: str = Path(..., description="Book name (supports abbreviations)", example="Psalms"),
chapter: int = Path(..., description="Chapter number", example=23, ge=1),
start: int = Path(..., description="Starting verse number", example=1, ge=1),
end: int = Path(..., description="Ending verse number", example=6, ge=1)
):
"""Get a range of verses with red letter information."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Check if book exists
all_books = bible.get_books()
if book not in all_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
# Check valid verse numbers
if chapter < 1 or start < 1 or end < 1:
raise HTTPException(status_code=404, detail="Invalid chapter or verse number")
verses = []
verse_texts = []
for verse_num in range(start, end + 1):
verse_text = bible.get_verse_text(book, chapter, verse_num)
if verse_text:
christ_words = get_christ_words(book, chapter, verse_num)
verses.append({
"verse": verse_num,
"text": verse_text,
"red_letter": christ_words
})
verse_texts.append(verse_text)
if not verses:
raise HTTPException(status_code=404, detail="Verse range not found")
return JSONResponse({
"book": book,
"chapter": chapter,
"start": start,
"end": end,
"reference": f"{book} {chapter}:{start}-{end}",
"verses": verses,
"text": " ".join(verse_texts)
})
@router.get("/interlinear/{book}/{chapter}/{verse}")
async def api_get_interlinear(
book: str = Path(..., description="Book name", example="John"),
chapter: int = Path(..., description="Chapter number", example=1),
verse: int = Path(..., description="Verse number", example=1)
):
"""Get interlinear (word-by-word) data for a verse."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Check if book exists
all_books = bible.get_books()
if book not in all_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
# Check valid chapter/verse numbers
if chapter < 1 or verse < 1:
raise HTTPException(status_code=404, detail="Invalid chapter or verse number")
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
raise HTTPException(status_code=404, detail="Verse not found")
if not has_interlinear_data(book, chapter, verse):
return JSONResponse({
"book": book,
"chapter": chapter,
"verse": verse,
"reference": f"{book} {chapter}:{verse}",
"text": verse_text,
"interlinear_available": False,
"words": []
})
interlinear_words = get_interlinear_data(book, chapter, verse)
return JSONResponse({
"book": book,
"chapter": chapter,
"verse": verse,
"reference": f"{book} {chapter}:{verse}",
"text": verse_text,
"interlinear_available": True,
"words": interlinear_words
})
@router.get("/books")
async def api_get_books():
"""Get list of all Bible books with metadata."""
books = bible.get_books()
old_testament = []
new_testament = []
for book in books:
chapters = [ch for bk, ch in bible.iter_chapters() if bk == book]
book_data = get_book_data(book) if has_book_data(book) else None
book_info = {
"name": book,
"chapters": len(chapters),
"testament": "Old Testament" if book in OT_BOOKS else "New Testament"
}
# Add metadata from book introductions if available
if book_data:
book_info["abbreviation"] = book_data.get("abbreviation")
book_info["category"] = book_data.get("category")
book_info["author"] = book_data.get("author")
book_info["position"] = book_data.get("position")
if book in OT_BOOKS:
old_testament.append(book_info)
else:
new_testament.append(book_info)
return {
"total_books": len(books),
"old_testament": old_testament,
"new_testament": new_testament
}
@router.get("/books/{book}")
async def api_get_book(book: str = Path(..., description="Book name", example="Genesis")):
"""Get details about a specific book including introduction and study material."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
chapters = [ch for bk, ch in bible.iter_chapters() if bk == book]
if not chapters:
raise HTTPException(status_code=404, detail="Book not found")
chapter_details = []
for chapter in chapters:
verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter]
chapter_details.append({
"chapter": chapter,
"verses": len(verses)
})
result = {
"name": book,
"total_chapters": len(chapters),
"chapters": chapter_details,
"links": {
"pdf": f"/api/books/{book}/pdf"
}
}
# Add book introduction data if available
book_data = get_book_data(book) if has_book_data(book) else None
if book_data:
result["abbreviation"] = book_data.get("abbreviation")
result["testament"] = book_data.get("testament")
result["position"] = book_data.get("position")
result["category"] = book_data.get("category")
result["author"] = book_data.get("author")
result["date_written"] = book_data.get("date_written")
result["introduction"] = book_data.get("introduction")
result["key_themes"] = book_data.get("key_themes")
result["key_verses"] = book_data.get("key_verses")
result["outline"] = book_data.get("outline")
result["historical_context"] = book_data.get("historical_context")
result["literary_style"] = book_data.get("literary_style")
result["christ_in_book"] = book_data.get("christ_in_book")
result["practical_application"] = book_data.get("practical_application")
return result
@router.get("/books/{book}/pdf")
async def api_book_pdf(book: str = Path(..., description="Book name", example="Genesis")):
"""Generate PDF for an entire Bible book."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
chapters = [ch for bk, ch in bible.iter_chapters() if bk == book]
if not chapters:
raise HTTPException(status_code=404, detail="Book not found")
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
# Prepare data for template
chapters_data = []
total_verses = 0
for chapter in chapters:
verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter]
if verses:
chapter_verses = []
for v in verses:
chapter_verses.append({
"verse": v.verse,
"text": v.text
})
chapters_data.append({
"chapter": chapter,
"verses": chapter_verses
})
total_verses += len(verses)
if not chapters_data:
raise HTTPException(status_code=404, detail="No verses found for this book")
# Render the PDF template
html_content = templates.get_template("book_pdf.html").render(
book=book,
chapters=chapters_data,
chapter_count=len(chapters_data),
verse_count=total_verses,
)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{create_slug(book)}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/books/{book}/chapters/{chapter}")
async def api_get_chapter(
book: str = Path(..., description="Book name", example="Romans"),
chapter: int = Path(..., description="Chapter number", example=8)
):
"""Get all verses in a chapter."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter]
if not verses:
raise HTTPException(status_code=404, detail="Chapter not found")
verse_list = []
for v in verses:
verse_list.append({
"verse": v.verse,
"text": v.text
})
return {
"book": book,
"chapter": chapter,
"total_verses": len(verses),
"verses": verse_list,
"links": {
"pdf": f"/api/books/{book}/chapters/{chapter}/pdf"
}
}
@router.get("/books/{book}/chapters/{chapter}/pdf")
async def api_chapter_pdf(
book: str = Path(..., description="Book name", example="Romans"),
chapter: int = Path(..., description="Chapter number", example=8)
):
"""Generate PDF for a specific Bible chapter."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter]
if not verses:
raise HTTPException(status_code=404, detail="Chapter not found")
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
# Prepare data for template
verse_list = []
for v in verses:
verse_list.append({
"verse": v.verse,
"text": v.text
})
# Render the PDF template
html_content = templates.get_template("chapter_pdf.html").render(
book=book,
chapter=chapter,
verses=verse_list,
verse_count=len(verses),
)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{create_slug(book)}-chapter-{chapter}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/books/{book}/text")
async def api_get_book_text(book: str = Path(..., description="Book name", example="Philemon")):
"""Get all text content of a book."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
verses = [v for v in bible.iter_verses() if v.book == book]
if not verses:
raise HTTPException(status_code=404, detail="Book not found")
chapters = {}
for v in verses:
if v.chapter not in chapters:
chapters[v.chapter] = []
chapters[v.chapter].append({
"verse": v.verse,
"text": v.text
})
chapter_list = []
for chapter_num in sorted(chapters.keys()):
chapter_list.append({
"chapter": chapter_num,
"verses": chapters[chapter_num]
})
return {
"book": book,
"total_chapters": len(chapters),
"total_verses": len(verses),
"chapters": chapter_list
}
@router.get("/bible")
async def api_get_bible():
"""Get the entire Bible text."""
books_data = {}
for v in bible.iter_verses():
if v.book not in books_data:
books_data[v.book] = {}
if v.chapter not in books_data[v.book]:
books_data[v.book][v.chapter] = []
books_data[v.book][v.chapter].append({
"verse": v.verse,
"text": v.text
})
books_list = []
for book_name in books_data:
chapter_list = []
for chapter_num in sorted(books_data[book_name].keys()):
chapter_list.append({
"chapter": chapter_num,
"verses": books_data[book_name][chapter_num]
})
books_list.append({
"book": book_name,
"chapters": chapter_list
})
total_verses = sum(len(books_data[book][ch]) for book in books_data for ch in books_data[book])
return {
"total_books": len(books_data),
"total_verses": total_verses,
"books": books_list
}
@router.get("/cross-references/{book}/{chapter}/{verse}")
async def api_get_cross_references(
book: str = Path(..., description="Book name", example="John"),
chapter: int = Path(..., description="Chapter number", example=3),
verse: int = Path(..., description="Verse number", example=16)
):
"""Get cross-references for a verse."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
raise HTTPException(status_code=404, detail="Verse not found")
cross_refs = get_cross_references(book, chapter, verse)
return {
"book": book,
"chapter": chapter,
"verse": verse,
"reference": f"{book} {chapter}:{verse}",
"cross_references": cross_refs
}
@router.get("/topics")
async def api_get_topics():
"""Get list of all topics."""
topics = get_all_topics()
topic_list = []
for topic_name, topic_data in topics.items():
topic_list.append({
"name": topic_name,
"slug": topic_name,
"description": topic_data.get("description", ""),
"subtopics": list(topic_data.get("subtopics", {}).keys())
})
return {
"total_topics": len(topics),
"topics": topic_list
}
@router.get("/topics/{topic_name}")
async def api_get_topic(topic_name: str = Path(..., description="Topic name", example="faith")):
"""Get details about a specific topic."""
topic = get_topic_with_text(topic_name)
if not topic:
raise HTTPException(status_code=404, detail="Topic not found")
return {
"name": topic_name,
"description": topic.get("description", ""),
"overview": topic.get("overview", ""),
"subtopics": topic.get("subtopics", {})
}
@router.get("/reading-plans")
async def api_get_reading_plans():
"""Get list of all reading plans."""
plans = get_plan_summary()
return {
"total_plans": len(plans),
"plans": plans
}
@router.get("/reading-plans/{plan_id}")
async def api_get_reading_plan(plan_id: str = Path(..., description="Reading plan ID", example="chronological")):
"""Get details about a specific reading plan."""
plan = get_plan(plan_id)
if not plan:
raise HTTPException(status_code=404, detail="Reading plan not found")
return plan
@router.get("/reading-plans/{plan_id}/day/{day_num}")
async def api_get_reading_plan_day_text(
plan_id: str = Path(..., description="Reading plan ID", example="paul-epistles"),
day_num: int = Path(..., description="Day number", example=1)
):
"""Get the Scripture text for a specific day in a reading plan."""
plan = get_plan(plan_id)
if not plan:
raise HTTPException(status_code=404, detail="Reading plan not found")
all_days = plan.get('days') or plan.get('sample_days', [])
# Find the specific day
day_data = None
for day in all_days:
if day['day'] == day_num:
day_data = day
break
if not day_data:
raise HTTPException(status_code=404, detail=f"Day {day_num} not found in plan")
# Import the get_reading_text function from reading_plans routes
from .reading_plans import get_reading_text
# Get the text for this day's readings
text_sections = get_reading_text(day_data['readings'])
# Convert Verse objects to dicts for JSON serialization
result = []
for section in text_sections:
result.append({
'book': section['book'],
'chapter': section['chapter'],
'reference': section['reference'],
'verses': [{'verse': v.verse, 'text': v.text} for v in section['verses']]
})
return {
'day': day_num,
'theme': day_data.get('theme', ''),
'readings': day_data['readings'],
'text': result
}
@router.get("/stories")
async def api_get_stories():
"""Get list of all Bible stories organized by category."""
categories = get_categories()
story_count = get_story_count()
category_count = get_category_count()
# Format categories for API response
categories_list = []
for category in categories:
stories_list = []
for story in category.get("stories", []):
stories_list.append({
"title": story.get("title"),
"slug": story.get("slug"),
"description": story.get("description"),
"verses": story.get("verses", []),
"characters": story.get("characters", []),
"themes": story.get("themes", []),
"kids_title": story.get("kids_title"),
"kids_description": story.get("kids_description"),
"has_kids_version": bool(story.get("kids_narrative"))
})
categories_list.append({
"category": category.get("category"),
"slug": category.get("slug"),
"description": category.get("description"),
"story_count": len(stories_list),
"stories": stories_list
})
return {
"total_stories": story_count,
"total_categories": category_count,
"categories": categories_list
}
@router.get("/stories/{slug}")
async def api_get_story(slug: str = Path(..., description="Story slug", example="creation-of-the-world")):
"""Get a specific Bible story by slug."""
story = get_story_by_slug(slug)
if not story:
raise HTTPException(status_code=404, detail="Story not found")
return {
"title": story.get("title"),
"slug": story.get("slug"),
"description": story.get("description"),
"category": story.get("category_name"),
"category_slug": story.get("category_slug"),
"verses": story.get("verses", []),
"characters": story.get("characters", []),
"themes": story.get("themes", []),
"narrative": story.get("narrative"),
"kids_title": story.get("kids_title"),
"kids_description": story.get("kids_description"),
"kids_narrative": story.get("kids_narrative"),
"has_kids_version": bool(story.get("kids_narrative")),
"links": {
"web": f"/stories/{slug}",
"kids_web": f"/stories/{slug}/kids" if story.get("kids_narrative") else None,
"pdf": f"/api/stories/{slug}/pdf",
"kids_pdf": f"/api/stories/{slug}/kids/pdf" if story.get("kids_narrative") else None
}
}
@router.get("/stories/{slug}/pdf")
async def api_story_pdf(slug: str = Path(..., description="Story slug")):
"""Generate PDF for a story (adult version)."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
story = get_story_by_slug(slug)
if not story:
raise HTTPException(status_code=404, detail="Story not found")
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
# Render the PDF template
html_content = templates.get_template("story_pdf.html").render(story=story)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{slug}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/stories/{slug}/kids/pdf")
async def api_story_kids_pdf(slug: str = Path(..., description="Story slug")):
"""Generate PDF for a story (kids version)."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
story = get_story_by_slug(slug)
if not story:
raise HTTPException(status_code=404, detail="Story not found")
if not story.get("kids_narrative"):
raise HTTPException(status_code=404, detail="Kids version not available for this story")
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
# Render the PDF template
html_content = templates.get_template("story_kids_pdf.html").render(story=story)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{slug}-kids.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
# ============================================================================
# RESOURCES ENDPOINTS
# ============================================================================
@router.get(
"/resources",
response_model=ResourcesListResponse,
summary="List all resource categories",
description="Get a list of all biblical resource categories (locations, angels, prophets, etc.)"
)
async def api_list_resource_categories():
"""List all available resource categories."""
def format_title(key: str) -> str:
"""Convert snake_case to Title Case."""
return key.replace('_', ' ').title()
def count_items(category_data: dict) -> int:
"""Count total items in a category (including nested subcategories)."""
count = 0
for value in category_data.values():
if isinstance(value, dict):
# Check if this is an item or a subcategory
if 'description' in value or 'verses' in value:
count += 1
else:
# It's a subcategory, recurse
count += count_items(value)
return count
categories = []
for cat_name, cat_data in RESOURCES_DATA.items():
# Create HTML URL by converting snake_case to kebab-case
html_url = f"/{cat_name.replace('_', '-')}"
categories.append({
"name": cat_name,
"title": format_title(cat_name),
"item_count": count_items(cat_data),
"url": f"/api/resources/{cat_name}",
"html_url": html_url
})
return {
"total_categories": len(categories),
"categories": categories
}
@router.get(
"/resources/{category}",
response_model=ResourceCategoryResponse,
summary="Get all items in a resource category",
description="Retrieve all items within a specific resource category",
responses={
200: {
"description": "Successfully retrieved category",
"content": {
"application/json": {
"example": {
"category": "biblical_locations",
"title": "Biblical Locations",
"total_items": 15,
"items": [
{
"name": "Garden of Eden",
"slug": "garden-of-eden",
"description": "The original home of mankind",
"verse_count": 2,
"url": "/api/resources/biblical_locations/garden-of-eden"
}
]
}
}
}
}
}
)
async def api_get_resource_category(
category: str = Path(..., description="Resource category name", example="biblical_locations")
):
"""Get all items in a specific resource category."""
if category not in RESOURCES_DATA:
raise HTTPException(status_code=404, detail=f"Resource category '{category}' not found")
cat_data = RESOURCES_DATA[category]
def format_title(key: str) -> str:
return key.replace('_', ' ').title()
def flatten_items(data: dict, parent_key: str = "") -> list:
"""Flatten nested resource structure into a list of items."""
items = []
for key, value in data.items():
if isinstance(value, dict):
if 'description' in value or 'verses' in value:
# This is an item
slug = _create_slug(key)
verse_count = len(value.get('verses', []))
items.append({
"name": key,
"slug": slug,
"description": value.get('description', ''),
"verse_count": verse_count,
"url": f"/api/resources/{category}/{slug}"
})
else:
# This is a subcategory, recurse
items.extend(flatten_items(value, key))
return items
items = flatten_items(cat_data)
return {
"category": category,
"title": format_title(category),
"total_items": len(items),
"items": items
}
@router.get(
"/resources/{category}/{slug}",
response_model=ResourceItemDetail,
summary="Get a specific resource item",
description="Retrieve detailed information about a specific resource item including all verses",
responses={
200: {
"description": "Successfully retrieved resource item",
"content": {
"application/json": {
"example": {
"name": "Garden of Eden",
"slug": "garden-of-eden",
"category": "biblical_locations",
"description": "The original home of mankind",
"verses": [
{
"reference": "Genesis 2:8",
"text": "And the LORD God planted a garden eastward in Eden; and there he put the man whom he had formed."
},
{
"reference": "Genesis 3:23",
"text": "Therefore the LORD God sent him forth from the garden of Eden, to till the ground from whence he was taken."
}
]
}
}
}
}
}
)
async def api_get_resource_item(
category: str = Path(..., description="Resource category name", example="biblical_locations"),
slug: str = Path(..., description="Resource item slug", example="garden-of-eden")
):
"""Get detailed information about a specific resource item."""
if category not in CATEGORY_TO_DATA:
raise HTTPException(status_code=404, detail=f"Resource category '{category}' not found")
cat_data = CATEGORY_TO_DATA[category]
# Search for the item by slug in potentially nested structure
def find_by_slug(data: dict, target_slug: str):
"""Recursively search for an item by slug."""
for key, value in data.items():
if isinstance(value, dict):
# Check if this is an item (has description or verses)
if 'description' in value or 'verses' in value:
if _create_slug(key) == target_slug:
return value, key
else:
# It's a subcategory, recurse
result = find_by_slug(value, target_slug)
if result:
return result
return None
result = find_by_slug(cat_data, slug)
if not result:
raise HTTPException(status_code=404, detail=f"Resource item '{slug}' not found in category '{category}'")
item_data, item_name = result
return {
"name": item_name,
"slug": slug,
"category": category,
"description": item_data.get('description', ''),
"verses": item_data.get('verses', [])
}
@router.get("/resources/{category}/pdf")
async def api_get_resource_category_pdf(
category: str = Path(..., description="Resource category name", example="biblical_locations")
):
"""Generate PDF for an entire resource category."""
# Check if category exists first (before checking WeasyPrint)
if category not in CATEGORY_TO_DATA:
raise HTTPException(status_code=404, detail=f"Resource category '{category}' not found")
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
cat_data = CATEGORY_TO_DATA[category]
def format_title(key: str) -> str:
return key.replace('_', ' ').title()
title = format_title(category)
# Render the PDF template
html_content = templates.get_template("resource_index_pdf.html").render(
resource_data=cat_data,
page_title=title,
page_subtitle=f"Biblical study resource",
page_description=f"Explore {title.lower()} from the King James Bible"
)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{category}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/resources/{category}/{slug}/pdf")
async def api_get_resource_item_pdf(
category: str = Path(..., description="Resource category name", example="biblical_locations"),
slug: str = Path(..., description="Resource item slug", example="garden-of-eden")
):
"""Generate PDF for a specific resource item."""
# Check if category exists first (before checking WeasyPrint)
if category not in CATEGORY_TO_DATA:
raise HTTPException(status_code=404, detail=f"Resource category '{category}' not found")
if not templates:
raise HTTPException(status_code=500, detail="Templates not initialized")
cat_data = CATEGORY_TO_DATA[category]
# Find the item
def find_by_slug(data: dict, target_slug: str):
for key, value in data.items():
if isinstance(value, dict):
if 'description' in value or 'verses' in value:
if _create_slug(key) == target_slug:
return value, key
else:
result = find_by_slug(value, target_slug)
if result:
return result
return None
result = find_by_slug(cat_data, slug)
if not result:
raise HTTPException(status_code=404, detail=f"Resource item '{slug}' not found")
# Now check WeasyPrint availability
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
item_data, item_name = result
def format_title(key: str) -> str:
return key.replace('_', ' ').title()
# Render the PDF template
html_content = templates.get_template("resource_detail_pdf.html").render(
item=item_data,
item_name=item_name,
category_name="", # Not used in simple template
resource_title=format_title(category)
)
# Generate PDF
pdf_buffer = await render_html_to_pdf_async(html_content)
# Return as downloadable PDF
filename = f"{slug}-{category}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get(
"/red-letter",
response_model=RedLetterListResponse,
summary="List all red letter verses",
description="Get a list of all verses containing the words of Jesus Christ (red letter edition). Supports filtering by book and pagination."
)
async def api_list_red_letter_verses(
book: Optional[str] = Query(None, description="Filter by book name", example="John"),
limit: int = Query(50, description="Maximum number of verses to return", example=50, ge=1, le=500),
offset: int = Query(0, description="Number of verses to skip", example=0, ge=0)
):
"""List all red letter verses with optional filtering and pagination."""
red_letter_data = load_red_letter_verses()
# Parse all verses and build result list
all_verses = []
for verse_ref, christ_words in red_letter_data.items():
# Parse the reference (format: "Book Chapter:Verse")
parts = verse_ref.rsplit(' ', 1)
if len(parts) != 2:
continue
book_name = parts[0]
chapter_verse = parts[1].split(':')
if len(chapter_verse) != 2:
continue
try:
chapter_num = int(chapter_verse[0])
verse_num = int(chapter_verse[1])
except ValueError:
continue
# Apply book filter if specified
if book and book_name != book:
# Try normalizing the book name in case it's an abbreviation
canonical = normalize_book_name(book)
if not canonical or book_name != canonical:
continue
# Get the verse text
verse_text = bible.get_verse_text(book_name, chapter_num, verse_num)
if not verse_text:
continue
all_verses.append({
"reference": verse_ref,
"book": book_name,
"chapter": chapter_num,
"verse": verse_num,
"text": verse_text,
"christ_words": christ_words,
"is_full_verse": christ_words == "full"
})
# Apply pagination
total = len(all_verses)
paginated_verses = all_verses[offset:offset + limit]
return {
"total": total,
"verses": paginated_verses,
"limit": limit,
"offset": offset
}
@router.get(
"/red-letter/stats",
response_model=RedLetterStatsResponse,
summary="Get red letter statistics",
description="Get statistics about verses containing the words of Jesus Christ, including counts by book and full vs. partial verses."
)
async def api_red_letter_stats():
"""Get statistics about red letter verses in the Bible."""
red_letter_data = load_red_letter_verses()
total_verses = len(red_letter_data)
full_verses = sum(1 for v in red_letter_data.values() if v == "full")
partial_verses = total_verses - full_verses
# Count by book
by_book = {}
books_set = set()
for verse_ref in red_letter_data.keys():
# Parse the reference (format: "Book Chapter:Verse")
parts = verse_ref.rsplit(' ', 1)
if len(parts) != 2:
continue
book_name = parts[0]
books_set.add(book_name)
by_book[book_name] = by_book.get(book_name, 0) + 1
# Sort books by count (descending)
by_book = dict(sorted(by_book.items(), key=lambda x: x[1], reverse=True))
return {
"total_verses": total_verses,
"full_verses": full_verses,
"partial_verses": partial_verses,
"books_with_red_letter": sorted(list(books_set)),
"by_book": by_book
}
@router.get(
"/verse/random",
response_model=VerseResponse,
summary="Get a random verse",
description="Returns a random Bible verse. Optionally filter by testament (ot/nt) or specific book."
)
async def api_random_verse(
testament: Optional[str] = Query(None, description="Filter by testament: 'ot' or 'nt'", example="nt"),
book: Optional[str] = Query(None, description="Filter by book name", example="John")
):
"""Get a random Bible verse with optional filtering."""
all_books = bible.get_books()
# Apply testament filter
if testament:
testament = testament.lower()
if testament == "ot":
filtered_books = [b for b in all_books if b in OT_BOOKS]
elif testament == "nt":
filtered_books = [b for b in all_books if b not in OT_BOOKS]
else:
raise HTTPException(status_code=400, detail="Testament must be 'ot' or 'nt'")
else:
filtered_books = all_books
# Apply book filter
if book:
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
if book not in filtered_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
filtered_books = [book]
# Select random book
selected_book = random.choice(filtered_books)
# Get random chapter
chapters = bible.get_chapters_for_book(selected_book)
if not chapters:
raise HTTPException(status_code=404, detail="No chapters found")
selected_chapter = random.choice(chapters)
# Get random verse
verses = bible.get_verses_by_book_chapter(selected_book, selected_chapter)
if not verses:
raise HTTPException(status_code=404, detail="No verses found")
selected_verse_num = random.choice([v.verse for v in verses])
# Get the verse text
verse_text = bible.get_verse_text(selected_book, selected_chapter, selected_verse_num)
if not verse_text:
raise HTTPException(status_code=404, detail="Verse text not found")
# Get red letter information
christ_words = get_christ_words(selected_book, selected_chapter, selected_verse_num)
return {
"book": selected_book,
"chapter": selected_chapter,
"verse": selected_verse_num,
"reference": f"{selected_book} {selected_chapter}:{selected_verse_num}",
"text": verse_text,
"red_letter": christ_words
}
@router.get(
"/commentary/{book}/{chapter}/{verse}",
response_model=CommentaryResponse,
summary="Get verse commentary",
description="Get AI-generated theological commentary for a specific verse, including analysis, historical context, and reflection questions."
)
async def api_get_verse_commentary(
book: str = Path(..., description="Book name", example="John"),
chapter: int = Path(..., description="Chapter number", example=3),
verse: int = Path(..., description="Verse number", example=16)
):
"""Get commentary for a specific verse."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Check if book exists
all_books = bible.get_books()
if book not in all_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
# Get verse text
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
raise HTTPException(status_code=404, detail="Verse not found")
# Load commentary data
commentary_data = _load_verse_commentary()
# Navigate to the commentary
if book not in commentary_data:
raise HTTPException(status_code=404, detail="Commentary not available for this book")
if chapter not in commentary_data[book]:
raise HTTPException(status_code=404, detail="Commentary not available for this chapter")
if verse not in commentary_data[book][chapter]:
raise HTTPException(status_code=404, detail="Commentary not available for this verse")
verse_commentary = commentary_data[book][chapter][verse]
return {
"book": book,
"chapter": chapter,
"verse": verse,
"reference": f"{book} {chapter}:{verse}",
"text": verse_text,
"analysis": verse_commentary.get("analysis", ""),
"historical": verse_commentary.get("historical", ""),
"questions": verse_commentary.get("questions", [])
}
@router.get(
"/chapter-commentary/{book}/{chapter}",
response_model=ChapterCommentaryResponse,
summary="Get chapter commentary",
description="Get a brief explanation of what a chapter contains and why it's significant."
)
async def api_get_chapter_commentary(
book: str = Path(..., description="Book name", example="Genesis"),
chapter: int = Path(..., description="Chapter number", example=1)
):
"""Get commentary/explanation for a specific chapter."""
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Check if book exists
all_books = bible.get_books()
if book not in all_books:
raise HTTPException(status_code=404, detail=f"Book '{book}' not found")
# Check if chapter exists
chapters = bible.get_chapters_for_book(book)
if chapter not in chapters:
raise HTTPException(status_code=404, detail=f"Chapter {chapter} not found in {book}")
# Get chapter explanation
if book in CHAPTER_EXPLANATIONS and chapter in CHAPTER_EXPLANATIONS[book]:
explanation = CHAPTER_EXPLANATIONS[book][chapter]
else:
# Provide generic explanation if specific one doesn't exist
explanation = f"Chapter {chapter} of {book}"
return {
"book": book,
"chapter": chapter,
"explanation": explanation
}
@router.post(
"/verses/bulk",
response_model=BulkVerseResponse,
summary="Bulk verse lookup",
description="Fetch multiple verses in a single request. Provide an array of verse references like ['John 3:16', 'Romans 8:28']."
)
async def api_bulk_verse_lookup(request: BulkVerseRequest):
"""Look up multiple verses in a single request."""
from ..kjv import VerseReference
verses = []
for ref_string in request.references:
try:
# Parse the reference
verse_ref = VerseReference.from_string(ref_string.strip())
# Normalize book name
book = verse_ref.book
canonical_name = normalize_book_name(book)
if canonical_name:
book = canonical_name
# Get verse text
verse_text = bible.get_verse_text(book, verse_ref.chapter, verse_ref.verse)
if verse_text:
# Get red letter information
christ_words = get_christ_words(book, verse_ref.chapter, verse_ref.verse)
verses.append({
"book": book,
"chapter": verse_ref.chapter,
"verse": verse_ref.verse,
"reference": f"{book} {verse_ref.chapter}:{verse_ref.verse}",
"text": verse_text,
"red_letter": christ_words
})
except Exception:
# Skip invalid references
continue
return {
"total": len(verses),
"verses": verses
}
@router.get(
"/family-tree/stats",
response_model=FamilyTreeStatsResponse,
summary="Get family tree statistics",
description="Get comprehensive statistics about the biblical family tree from the GEDCOM genealogy data."
)
async def api_family_tree_stats():
"""Get statistics about the biblical family tree from GEDCOM data."""
from ..routes.family_tree import get_family_tree_data
import re
try:
family_tree_data, generations = get_family_tree_data()
if not family_tree_data:
raise HTTPException(status_code=500, detail="Family tree data not available")
# Load biographies for supplemental age data
biographies_data = _load_biographies()
biographies = biographies_data.get("biographies", {})
aliases = biographies_data.get("aliases", {})
# Load known close family marriages
known_marriages_data = _load_close_family_marriages()
known_marriages = known_marriages_data.get("marriages", [])
# Calculate statistics
total_people = len(family_tree_data)
total_generations = len(generations) if generations else 0
# Find longest lived person
longest_lived_person = None
longest_lived_person_id = None
longest_lifespan = 0
# Find person with most children
most_children_person = None
most_children_person_id = None
most_children_count = 0
# Find person with most siblings
most_siblings_person = None
most_siblings_person_id = None
most_siblings_count = 0
# Track close family marriages
close_family_marriages_count = 0
# Calculate average lifespan
total_age = 0
people_with_ages = 0
for person_id, person in family_tree_data.items():
# Check lifespan - try multiple formats
age = None
# Try age_at_death field first
if person.get("age_at_death") and person["age_at_death"] != "Unknown":
try:
# Parse age (format: "123 years")
age_str = person["age_at_death"].replace(" years", "").strip()
age = int(age_str)
except (ValueError, AttributeError):
pass
# Also try death_year field which might contain "Lived XXX years"
if age is None and person.get("death_year") and person["death_year"] != "Unknown":
try:
death_text = person["death_year"]
if "Lived" in death_text and "years" in death_text:
# Format: "Lived 930 years"
match = re.search(r'Lived (\d+) years', death_text)
if match:
age = int(match.group(1))
except (ValueError, AttributeError):
pass
# Finally, check biographies.json for age data
if age is None:
person_name = person.get("name")
# Check if name is an alias
lookup_name = aliases.get(person_name, person_name)
if lookup_name in biographies:
try:
biography = biographies[lookup_name]
key_events = biography.get("key_events", [])
# Find death event (usually the last event with highest age)
death_age = 0
for event in key_events:
event_age = event.get("age")
if event_age is not None and event_age > death_age:
death_age = event_age
if death_age > 0:
age = death_age
except (ValueError, AttributeError, KeyError):
pass
# Record age statistics if we found an age
if age is not None:
total_age += age
people_with_ages += 1
if age > longest_lifespan:
longest_lifespan = age
longest_lived_person = person
longest_lived_person_id = person_id
# Check children count
children_count = len(person.get("children", []))
if children_count > most_children_count:
most_children_count = children_count
most_children_person = person
most_children_person_id = person_id
# Check siblings count
siblings_count = len(person.get("siblings", []))
if siblings_count > most_siblings_count:
most_siblings_count = siblings_count
most_siblings_person = person
most_siblings_person_id = person_id
# Check for close family marriages (if person has spouse)
if person.get("spouse"):
spouse_name = person.get("spouse")
# Check if spouse is in the family tree
for potential_spouse_id, potential_spouse in family_tree_data.items():
if potential_spouse.get("name") == spouse_name:
# Check if they share parents (siblings)
person_parents = set(person.get("parents", []))
spouse_parents = set(potential_spouse.get("parents", []))
if person_parents and spouse_parents and person_parents & spouse_parents:
# They share at least one parent - siblings or half-siblings
close_family_marriages_count += 0.5 # Count each marriage once (will be seen from both sides)
# Check if spouse is parent's sibling (aunt/uncle-niece/nephew)
for parent_id in person.get("parents", []):
if parent_id in family_tree_data:
parent_siblings = family_tree_data[parent_id].get("siblings", [])
if potential_spouse_id in parent_siblings:
close_family_marriages_count += 0.5
break
# Calculate average lifespan
average_lifespan = round(total_age / people_with_ages, 1) if people_with_ages > 0 else None
# Add known biblical close family marriages to the count
close_family_marriages_count += len(known_marriages)
# Build response
return {
"total_people": total_people,
"total_generations": total_generations,
"longest_lived": {
"name": longest_lived_person["name"] if longest_lived_person else "Unknown",
"person_id": longest_lived_person_id if longest_lived_person_id else "unknown",
"value": longest_lifespan,
"additional_info": f"Lived {longest_lifespan} years" if longest_lived_person else None
},
"most_children": {
"name": most_children_person["name"] if most_children_person else "Unknown",
"person_id": most_children_person_id if most_children_person_id else "unknown",
"value": most_children_count,
"additional_info": f"Had {most_children_count} children" if most_children_person else None
},
"most_siblings": {
"name": most_siblings_person["name"] if most_siblings_person else "Unknown",
"person_id": most_siblings_person_id if most_siblings_person_id else "unknown",
"value": most_siblings_count,
"additional_info": f"Had {most_siblings_count} siblings" if most_siblings_person else None
},
"average_lifespan": average_lifespan,
"total_with_known_ages": people_with_ages,
"close_family_marriages": int(close_family_marriages_count)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to load family tree statistics: {str(e)}")
@router.get(
"/family-tree",
response_model=FamilyTreeListResponse,
summary="List all biblical figures",
description="Get a list of all people who have biographies in the family tree database."
)
async def api_list_family_tree():
"""List all people with biographies."""
data = _load_biographies()
biographies = data.get("biographies", {})
people = sorted(list(biographies.keys()))
return {
"total": len(people),
"people": people
}
@router.get(
"/family-tree/{name}",
response_model=BiographyResponse,
summary="Get biography of biblical figure",
description="Get detailed biography including summary, significance, and key life events for a specific biblical figure."
)
async def api_get_biography(
name: str = Path(..., description="Name of the person", example="Abraham")
):
"""Get biography of a specific person."""
data = _load_biographies()
biographies = data.get("biographies", {})
aliases = data.get("aliases", {})
# Check if name is an alias
if name in aliases:
name = aliases[name]
# Get biography
if name not in biographies:
raise HTTPException(status_code=404, detail=f"Biography for '{name}' not found")
biography = biographies[name]
return {
"name": name,
"summary": biography.get("summary", ""),
"significance": biography.get("significance", ""),
"key_events": biography.get("key_events", [])
}
# =============================================================================
# Strong's Concordance Endpoints
# =============================================================================
@router.get(
"/strongs/{strongs_number}",
summary="Get Strong's concordance entry",
description="Look up a Strong's number (e.g., H1, G3056) and get the full definition, etymology, and KJV usage.",
responses={
200: {
"description": "Strong's entry found",
"content": {
"application/json": {
"example": {
"strongs": "G3056",
"language": "Greek",
"word": "λόγος",
"transliteration": "lógos",
"definition": "something said (including the thought)",
"kjv_usage": "account, cause, communication, doctrine, saying, word",
"derivation": "from G3004 (λέγω);"
}
}
}
},
404: {"description": "Strong's number not found"}
}
)
async def api_get_strongs(
strongs_number: str = Path(
...,
description="Strong's number (H1-H8674 for Hebrew, G1-G5624 for Greek)",
example="G3056"
)
):
"""Look up a Strong's concordance entry."""
entry = format_strongs_entry(strongs_number)
if not entry:
raise HTTPException(
status_code=404,
detail=f"Strong's number '{strongs_number}' not found. Use H1-H8674 for Hebrew or G1-G5624 for Greek."
)
return entry
@router.get(
"/strongs",
summary="Search Strong's concordance",
description="Search Hebrew and Greek dictionaries by definition or KJV usage.",
responses={
200: {
"description": "Search results",
"content": {
"application/json": {
"example": {
"query": "love",
"language": "both",
"total": 25,
"results": [
{
"strongs": "G26",
"language": "Greek",
"word": "ἀγάπη",
"definition": "love, i.e. affection or benevolence",
"kjv_usage": "charity, dear, love"
}
]
}
}
}
}
}
)
async def api_search_strongs(
q: str = Query(..., description="Search query", example="love"),
language: str = Query(
"both",
description="Language to search: 'hebrew', 'greek', or 'both'",
example="both"
),
limit: int = Query(50, description="Maximum results", ge=1, le=200)
):
"""Search Strong's concordance by definition or KJV usage."""
results = search_strongs(q, language=language.lower(), limit=limit)
return {
"query": q,
"language": language,
"total": len(results),
"results": results
}
@lru_cache(maxsize=1)
def _get_site_stats():
"""Generate comprehensive site statistics. Cached since data rarely changes."""
data_dir = FilePath(__file__).parent.parent / "data"
# Bible statistics
total_verses = bible.get_verse_count()
total_books = len(bible.get_books())
total_chapters = len(bible.get_chapters())
# Calculate words in Bible
total_words = sum(len(verse.text.split()) for verse in bible.iter_verses())
# Count unique book types
ot_books = len(OT_BOOKS)
nt_books = len(NT_BOOKS)
# Data file statistics
total_json_files = len(list(data_dir.glob('**/*.json')))
# Verse commentary statistics
verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json')))
total_commentary_verses = 0
total_commentary_words = 0
for file in (data_dir / 'verse_commentary').glob('*.json'):
data = json.load(open(file))
commentary = data.get('commentary', {})
for chapter in commentary.values():
for verse_data in chapter.values():
total_commentary_verses += 1
analysis = verse_data.get('analysis', '')
historical = verse_data.get('historical', '')
clean_analysis = re.sub(r'<[^>]+>', '', analysis)
clean_historical = re.sub(r'<[^>]+>', '', historical)
total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split())
# Cross-reference statistics
cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json')))
total_cross_refs = 0
verses_with_cross_refs = 0
for file in (data_dir / 'cross_references').glob('*.json'):
data = json.load(open(file))
verses_with_cross_refs += len(data)
for verse_refs in data.values():
total_cross_refs += len(verse_refs)
# Red letter statistics
red_letter_data = json.load(open(data_dir / 'red_letter_verses.json'))
total_red_letter_verses = len(red_letter_data['verses'])
# Study resources
study_guide_files = len(list((data_dir / 'study_guides').glob('*.json')))
topic_files = len(list((data_dir / 'topics').glob('*.json')))
resource_files = len(list((data_dir / 'resources').glob('*.json')))
story_files = len(list((data_dir / 'stories').glob('*.json')))
# Interlinear data size
interlinear_file = data_dir / 'interlinear.json.gz'
interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0
# Calculate total data directory size
total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file())
total_data_size_mb = total_data_size / 1024 / 1024
# Book abbreviations
bible_metadata_file = data_dir / 'bible_metadata.json'
bible_metadata = json.load(open(bible_metadata_file))
total_abbreviations = len(bible_metadata.get('book_abbreviations', {}))
# Biographies
bio_data = json.load(open(data_dir / 'biographies.json'))
total_biographies = len(bio_data.get('biographies', {}))
# Reading plans
reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json')))
# Strong's concordance
strongs_dir = data_dir / 'strongs'
if strongs_dir.exists():
hebrew_data = json.load(open(strongs_dir / 'hebrew.json'))
greek_data = json.load(open(strongs_dir / 'greek.json'))
total_hebrew_entries = len(hebrew_data)
total_greek_entries = len(greek_data)
else:
total_hebrew_entries = 0
total_greek_entries = 0
return {
'bible': {
'total_verses': total_verses,
'total_books': total_books,
'ot_books': ot_books,
'nt_books': nt_books,
'total_chapters': total_chapters,
'total_words': total_words,
'avg_words_per_verse': round(total_words / total_verses, 1),
'avg_verses_per_chapter': round(total_verses / total_chapters, 1),
},
'commentary': {
'files': verse_commentary_files,
'verses_covered': total_commentary_verses,
'total_words': total_commentary_words,
'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0,
'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1),
},
'cross_references': {
'files': cross_reference_files,
'verses_with_refs': verses_with_cross_refs,
'total_references': total_cross_refs,
'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0,
'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1),
},
'red_letter': {
'total_verses': total_red_letter_verses,
'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1),
},
'study_resources': {
'study_guides': study_guide_files,
'topics': topic_files,
'resources': resource_files,
'stories': story_files,
'biographies': total_biographies,
'reading_plans': reading_plan_files,
},
'language_tools': {
'hebrew_entries': total_hebrew_entries,
'greek_entries': total_greek_entries,
'total_strongs': total_hebrew_entries + total_greek_entries,
'interlinear_size_mb': round(interlinear_size_mb, 1),
},
'data': {
'total_json_files': total_json_files,
'total_size_mb': round(total_data_size_mb, 1),
'book_abbreviations': total_abbreviations,
}
}
@router.get(
"/stats",
summary="Site statistics",
description="Comprehensive statistics about KJV Study - Bible data, commentary, cross-references, and study resources.",
responses={
200: {
"description": "Site statistics",
"content": {
"application/json": {
"example": {
"bible": {
"total_verses": 31102,
"total_books": 66,
"ot_books": 39,
"nt_books": 27,
"total_chapters": 1189,
"total_words": 789629,
"avg_words_per_verse": 25.4,
"avg_verses_per_chapter": 26.2
},
"commentary": {
"files": 66,
"verses_covered": 31102,
"total_words": 5000000,
"avg_words_per_verse": 160.7,
"coverage_percent": 100.0
}
}
}
}
}
}
)
async def api_stats():
"""Get comprehensive site statistics."""
return _get_site_stats()