Sometimes people don’t want to hear the truth because they don’t want their illusions destroyed, Friedrich Nietzsche.
Consistency is key when building a productive digital environment. For my daily workflow, I wanted the terminal greetings to be not just informative, but spiritually grounding. Hardcoding a quote every day is tedious, and relying solely on an internet connection can be fragile.
To solve this, I wrote bible_verse.py. This module acts as the religious/content engine for my greeting scripts, designed to be robust and flexible. It attempts to retrieve a verse through a cascading series of sources:
The script below is the implementation of this logic, featuring graceful dependency handling and colored terminal output.

"""
bible_verse.py — Bible verse of the day for greetings.py
──────────────────────────────────────────────────────────
This module supplies a daily Bible verse for terminal greeting scripts.
It tries multiple sources in a cascading sequence, or optionally prints
all available sources.
Sources (in order of preference when `full=False`):
1. YouVersion Bible API (online)
— Verse of the day from YouVersion's community‑curated plan.
— Requires an API key stored in password‑store (`pass apps/yvp_app_key`).
2. Our Manna Daily Bread API (online)
— Devotional verse from ourmanna.org.
3. Personal daily plan (local file + offline/online lookup)
— Reads ~/dotfiles/myArch/config/bible/daily_plan.txt
(one Bible reference per line; supports ranges like "Psalm 23:1‑3").
— Offline lookups are performed with:
• `diatheke` (Sword project, KJV module engKJV2006eb)
• `freebible` Python library (`bibles.web.quote`, KJV)
— If both offline lookups fail, falls back to bible‑api.com public API.
4. Hardcoded fallback verse (always available)
— John 3:16 (KJV) when all other sources fail.
Integration:
from bible_verse import verse_of_the_day
verse_of_the_day() # first available source
verse_of_the_day(full=True) # print all sources
Dependencies (all optional; the module degrades gracefully):
pip install requests freebible colorama
External tools:
- diatheke (from the Sword project) must be on PATH.
- password‑store (`pass`) for YouVersion API key (optional).
Optional colour support:
pip install colorama
"""
import textwrap
import datetime
import html
from pathlib import Path
import re
import subprocess
# ----------------------------------------------------------------------
# Optional dependencies — we handle ImportError gracefully so the
# module can still be imported even if a particular library is missing.
# The actual functions fall back to other sources when unavailable.
# ----------------------------------------------------------------------
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# ----------------------------------------------------------------------
# freebible is imported early because it's used as a core offline source.
# If it's missing, the module will raise ImportError – consider wrapping
# the import in a try/except if you need the module to load without it.
# ----------------------------------------------------------------------
try:
from freebible import bibles
HAS_BIBLES = True
except ImportError:
HAS_BIBLES = False
bibles = None
try:
from colorama import Fore, Style
HAS_COLOR = True
except ImportError:
# If colorama is missing, provide dummy classes so that formatting
# codes are empty strings and nothing breaks.
class Fore: # type: ignore
CYAN = YELLOW = GREEN = MAGENTA = RESET = ""
class Style: # type: ignore
RESET_ALL = ""
HAS_COLOR = False
# ── Configuration ─────────────────────────────────────────────────────────────
# Your personal reading plan — one reference per line
# Format examples: Genesis 1:1 John 3:16 Psalm 23:1
DAILY_PLAN_FILE = Path.home() / "dotfiles/myArch/config/bible/daily_plan.txt"
"""Path to a plain-text file containing a personal Bible reading plan."""
# Fallback verse when nothing else works
FALLBACK_VERSE = {
"ref": "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.",
"source": "KJV (hardcoded fallback)",
}
"""Hardcoded dictionary providing a known verse when both network and offline database fail."""
# Width for text wrapping
WRAP_WIDTH = 72
"""Maximum line length when printing the verse text."""
# ----------------------------------------------------------------------
# Book name → Sword / freebible abbreviation mapping
# Keys are full book names as they might appear in a daily plan.
# Values are abbreviations used by diatheke and freebible.
# Extended to cover all canonical books.
# ----------------------------------------------------------------------
"""
>>> from freebible import bibles
# This is the internal book‑name vocabulary used by our installed version of freebible.
# It is not using full names like "1 Peter" or "Psalms" — instead it uses its own abbreviations,
# and these are the only valid keys when calling bibles.web.quote() or accessing the internal data.
>>> print(bibles.web.book_map.keys())
dict_keys(['Gen', 'Exo', 'Lev', 'Num', 'Deut', 'Josh', 'Judg', 'Rth', '1 Sam', '2 Sam', '1 Kgs', '2 Kgs', '1 Chron', '2 Chron', 'Ezra', 'Neh', 'Esth', 'Job', 'Pslm', 'Prov', 'Eccles', 'Song', 'Isa', 'Jer', 'Lam', 'Ezek', 'Dan', 'Hos', 'Joel', 'Amos', 'Obad', 'Jnh', 'Micah', 'Nah', 'Hab', 'Zeph', 'Haggai', 'Zech', 'Mal', 'Matt', 'Mrk', 'Luk', 'John', 'Acts', 'Rom', '1 Cor', '2 Cor', 'Gal', 'Ephes', 'Phil', 'Col', '1 Thess', '2 Thess', '1 Tim', '2 Tim', 'Titus', 'Philem', 'Hebrews', 'James', '1 Pet', '2 Pet', '1 John', '2 John', '3 John', 'Jude', 'Rev'])
"""
"""Mapping from full book names (as they appear in the plan) to Sword
module abbreviations. The abbreviations are used by the `diatheke` tool
and the `bibles` Python library."""
BOOK_MAP = {
# Pentateuch
"Genesis": "Gen",
"Exodus": "Exo",
"Leviticus": "Lev",
"Numbers": "Num",
"Deuteronomy": "Deut",
# History
"Joshua": "Josh",
"Judges": "Judg",
"Ruth": "Rth",
"1 Samuel": "1 Sam",
"2 Samuel": "2 Sam",
"1 Kings": "1 Kgs",
"2 Kings": "2 Kgs",
"1 Chronicles": "1 Chron",
"2 Chronicles": "2 Chron",
"Ezra": "Ezra",
"Nehemiah": "Neh",
"Esther": "Esth",
# Wisdom
"Job": "Job",
"Psalms": "Pslm",
"Proverbs": "Prov",
"Ecclesiastes": "Eccles",
"Song of Solomon": "Song",
# Major Prophets
"Isaiah": "Isa",
"Jeremiah": "Jer",
"Lamentations": "Lam",
"Ezekiel": "Ezek",
"Daniel": "Dan",
# Minor Prophets
"Hosea": "Hos",
"Joel": "Joel",
"Amos": "Amos",
"Obadiah": "Obad",
"Jonah": "Jnh",
"Micah": "Micah",
"Nahum": "Nah",
"Habakkuk": "Hab",
"Zephaniah": "Zeph",
"Haggai": "Haggai",
"Zechariah": "Zech",
"Malachi": "Mal",
# Gospels
"Matthew": "Matt",
"Mark": "Mrk",
"Luke": "Luk",
"John": "John",
# Acts
"Acts": "Acts",
# Paul
"Romans": "Rom",
"1 Corinthians": "1 Cor",
"2 Corinthians": "2 Cor",
"Galatians": "Gal",
"Ephesians": "Ephes",
"Philippians": "Phil",
"Colossians": "Col",
"1 Thessalonians": "1 Thess",
"2 Thessalonians": "2 Thess",
"1 Timothy": "1 Tim",
"2 Timothy": "2 Tim",
"Titus": "Titus",
"Philemon": "Philem",
# General Epistles
"Hebrews": "Hebrews",
"James": "James",
"1 Peter": "1 Pet",
"2 Peter": "2 Pet",
"1 John": "1 John",
"2 John": "2 John",
"3 John": "3 John",
"Jude": "Jude",
# Revelation
"Revelation": "Rev",
}
# ── Helpers ───────────────────────────────────────────────────────────────────
def _print_verse(verse: dict | None) -> None:
"""
Print a verse dictionary with colour and consistent formatting.
Args:
verse: Dictionary with keys 'ref', 'text', 'source'.
If None, the function prints nothing and returns False.
Returns:
False when verse is None, otherwise prints and returns None (implicitly).
This return convention is used by the caller to detect whether anything was printed.
"""
if not verse:
return False
ref = verse.get("ref", "")
text = verse.get("text", "")
source = verse.get("source", "")
print(f"\n{Fore.CYAN}📖 Bible Verse of the Day{Style.RESET_ALL}")
print(f" {Fore.YELLOW}{ref}{Style.RESET_ALL}")
print()
# Split on double newlines to respect verse paragraphing (e.g. from the reading plan)
for para in text.split("\n\n"):
# Wrap the verse text at WRAP_WIDTH, indented 4 spaces
for line in textwrap.wrap(para, width=WRAP_WIDTH):
print(f" {line}")
print()
if source:
# Use a slightly dimmed style for the attribution; we fall back
# to ANSI escape code if colorama is not available.
print(f"\n {Fore.CYAN}{Style.RESET_ALL if not HAS_COLOR else ''}"
f" \033[2m— {source}\033[0m")
print()
# ── Source 1: YouVersion & Our Manna ──────────────────────────────────────
# --- YouVersion API helpers ---
# echo "YOUVERSION-API-KEY" | pass insert -f apps/yvp_app_key
# Alternative 1: echo 'export YYOUVERSION-API-KEY="YOUR_APP_KEY_HERE"' >> ~/.zshrc
# Alternative 2: # .env file in your project (add to .gitignore!)
# YOUVERSION-API-KEY=your_actual_key_here
# >>> from dotenv import load_dotenv
# >>> load_dotenv()
# >>> app_key = os.getenv("YVP_APP_KEY")
def _get_youversion_headers() -> dict[str, str]:
"""
Retrieve YouVersion API credentials from the system password store.
Uses the `pass` command to read the key stored at `apps/yvp_app_key`.
Returns:
A dict suitable for the `headers` parameter of requests, e.g.
{"YOUVERSION-API-KEY": "YOUR_APP_KEY_HERE"}
"""
app_key = subprocess.check_output(
["pass", "apps/yvp_app_key"],
text=True,
).strip()
return {
"X-YVP-App-Key": app_key,
}
def _day_of_year() -> int:
"""
Return the current day of year (1‑366).
"""
return datetime.date.today().timetuple().tm_yday
def _youversion_passage_id(day: int) -> str | None:
"""
Fetch the passage ID for a given day from YouVersion's verse_of_the_days endpoint.
Args:
day: Day number within the year (1‑366).
Returns:
A passage identifier like "JHN.3.16", or None on failure.
"""
try:
headers = _get_youversion_headers()
url = f"https://api.youversion.com/v1/verse_of_the_days/{day}"
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
return data.get("passage_id")
except Exception:
return None
def _youversion_passage(
passage_id: str,
bible_id: int = 3034,
) -> dict | None:
"""
Retrieve the actual text and reference for a YouVersion passage.
Args:
passage_id: Passage identifier, e.g. "JHN.3.16".
bible_id: Numeric ID of the Bible version (default 3034 = KJV).
Returns:
Dictionary with keys 'ref', 'text', 'source', or None on failure.
"""
try:
headers = _get_youversion_headers()
url = f"https://api.youversion.com/v1/bibles/{bible_id}/passages/{passage_id}"
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
return {
"ref": html.unescape(
str(data.get("reference", "")).strip()
),
"text": html.unescape(
str(data.get("content", "")).strip()
),
"source": "YouVersion",
}
except Exception:
return None
def _from_youversion() -> dict | None:
"""
Fetch today's Bible verse from YouVersion's official API.
Returns:
A dict with 'ref', 'text', 'source', or None if unavailable.
"""
day = _day_of_year()
passage_id = _youversion_passage_id(day)
if not passage_id:
return None
return _youversion_passage(passage_id)
# --- Our Manna API ---
def _from_ourmanna_api() -> dict | None:
"""
Fallback: Fetch the verse of the day from OurManna.com.
curl --request GET \
--url 'https://beta.ourmanna.com/api/v1/get?format=json&order=daily' \
--header 'accept: application/json'
API response
{
"verse": {
"details": {
"text": "To all perfection I see a limit, but your commands are boundless.",
"reference": "Psalm 119:96",
"version": "NIV",
"verseurl": "http://www.ourmanna.com/"
},
"notice": "Powered by OurManna.com"
}
}
Returns:
Dictionary with keys 'ref', 'text', 'source', or None on error.
"""
if not HAS_REQUESTS:
return None
url = "https://beta.ourmanna.com/api/v1/get?format=json&order=daily"
try:
resp = requests.get(url, timeout=10,
headers={"User-Agent": "greetings/1.0"})
resp.raise_for_status()
data = resp.json()
details = data.get("verse", {}).get("details", {})
text = html.unescape(str(details.get("text", "")).strip())
ref = html.unescape(str(details.get("reference", "")).strip())
if not ref or not text:
return None
return {
"ref": ref,
"text": text,
"source": "OurManna.com",
}
except Exception:
return None
# ── Source 2: Daily plan + freebible KJV ──────────────────────────────────────
def _get_book_chapter_verses(
reference: str,
) -> tuple[str, int, list[int]] | None:
"""
Parse a Bible reference string into its components.
Supports formats like:
John 3:16
1 Peter 5:6-7
Psalm 23:1-3
Args:
reference: A string containing a Bible reference.
Returns:
A tuple (book_key, chapter, [verse_numbers]) where:
- book_key is the Sword module abbreviation (from BOOK_MAP)
- chapter is an integer
- [verse_numbers] is a list of verse integers (single verse
yields a one-element list, ranges are expanded).
Returns None if the reference cannot be parsed.
"""
# Attempt to match a reference of the form:
# :[-]
# Captures:
# group 1 → book name (with optional leading digit, e.g. "1 Peter")
# group 2 → chapter number
# group 3 → starting verse
# group 4 → optional ending verse
m = re.match(
r"^(\d?\s?[A-Za-z]+(?:\s[A-Za-z]+)*)\s+(\d+):(\d+)(?:-(\d+))?$",
reference.strip(), # remove surrounding whitespace before matching
re.IGNORECASE, # allow case-insensitive book names
)
# If the pattern does not match, the reference is invalid
if not m:
return None
# Extract the raw book name and normalize it to title case
# (e.g. "1 peter" → "1 Peter")
# The title() method returns a string where the first character in every word is upper case
raw_book = m.group(1).strip().title() # normalise to title case
# Look up the normalized book name in BOOK_MAP to obtain the
# canonical abbreviation used by the Bible module.
# (e.g., "1 Peter" → "1 Pet")
book = BOOK_MAP.get(raw_book)
if not book:
# Unknown or unmapped book name
return None
# Convert the captured chapter number to an integer
chapter = int(m.group(2))
# Parse the starting verse
start_verse = int(m.group(3))
# Parse the ending verse if present; otherwise use the starting verse
end_verse = int(m.group(4) or start_verse)
# Expand the verse range into a list of integers
# e.g. 6–7 → [6, 7], or 16 → [16]
verses = list(range(start_verse, end_verse + 1))
# Return the fully parsed reference components
return book, chapter, verses
def _load_daily_plan(path: Path) -> list[str]:
"""
Read the daily plan file and return a list of reference strings.
Lines starting with # are treated as comments and skipped.
Empty lines are skipped.
Args:
path: Path to the daily plan file.
File format (daily_plan.txt):
# Old Testament
Genesis 1:1
Genesis 1:2
# New Testament
John 3:16
Romans 8:28
Returns:
A list of verse references (e.g. ["Genesis 1:1", "John 3:16"]).
"""
# If the file does not exist, return an empty list
if not path.exists():
return []
refs = [] # accumulator for valid reference lines
# Read the file as UTF‑8 and iterate over each line
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip() # Read the file as UTF‑8 and iterate over each line
# Skip empty lines and comment lines beginning with '#'
if line and not line.startswith("#"):
refs.append(line) # keep valid reference lines
# Return the collected reference strings
return refs
def _lookup_freebible(
book: str,
chapter: int,
verse: int,
) -> str | None:
"""
Look up a single verse using the `freebible` library's web API (KJV).
This package provides a web‑based lookup (defaults to KJV).
The `bibles.web.quote()` function takes a book abbreviation,
chapter, and verse and returns a verse object whose string
representation is the text.
Args:
book: Sword module book abbreviation (e.g. "Gen").
chapter: Chapter number.
verse: Verse number.
Returns:
The verse text as a plain string, or None on any error.
"""
# If the bibles package is not available, abort immediately
if not HAS_BIBLES:
return None
try:
# Perform the lookup using the web-based API.
# Returns a verse object whose __str__() yields the text.
# >>> bibles.web.quote("John", 1, 1)
# John 1:1 In the beginning was the Word, and the Word was with God, and the Word was God.
verse_obj = bibles.web.quote(book, chapter, verse)
# Convert to plain string and strip surrounding whitespace.
return str(verse_obj).strip()
except Exception:
# Any failure (network, invalid reference, missing module)
# results in a clean None return.
return None
def _lookup_diatheke(
book: str,
chapter: int,
verse: int,
) -> str | None:
"""
Look up a single verse using the `diatheke` command-line tool.
diatheke is part of the Sword project. The module used here is
engKJV2006eb (an electronic edition of the KJV).
Args:
book: Sword module book abbreviation (e.g. "Gen").
chapter: Chapter number.
verse: Verse number.
Returns:
The cleaned verse text (plain ASCII, no markup), or None if the
lookup fails (diatheke not found, invalid reference, empty output).
"""
# Construct a diatheke-style reference string, e.g. "Gen 1:1"
ref = f"{book} {chapter}:{verse}"
try:
# Invoke diatheke with:
# -b engKJV2006eb → select the KJV module
# -k → lookup key
# stderr suppressed to avoid noise on missing modules, etc.
out = subprocess.check_output(
["diatheke", "-b", "engKJV2006eb", "-k", ref],
stderr=subprocess.DEVNULL,
text=True, # return output as str instead of bytes
).strip()
# If diatheke produced no output, treat as failure
if not out:
return None
# diatheke output typically starts with "ModuleName: verse text", "engKJV2006eb: In the beginning..."
# We extract everything after the colon.
first_line = out.splitlines()[0].strip()
# Extract everything after the first colon.
# Pattern: :
m = re.match(r"^[^:]+:\s*(.*)$", first_line)
if not m:
return None
# Raw verse text (may still contain Sword markup)
text = m.group(1).strip()
# Remove any remaining Sword markup ( tags, etc.)
# Remove word-level tags
text = re.sub(r"]*>" , "", text)
text = text.replace("", "")
# Remove any remaining XML/HTML-like tags
text = re.sub(r"<[^>]+>", "", text)
# Collapse all whitespace sequences to single spaces
return " ".join(text.split())
except Exception:
# Covers: diatheke not installed, bad exit code, invalid reference,
# Unicode issues, etc.
return None
def _lookup_bible_api(ref: str) -> str | None:
"""
Look up a Bible reference using the public bible‑api.com endpoint.
This is used as a fallback when both offline sources (diatheke,
freebible) fail for a reading plan entry.
Args:
ref: A human‑readable reference string, e.g. "John 3:16".
Returns:
The full text of the passage, or None on any error.
"""
try:
r = requests.get(
f"https://bible-api.com/{ref.replace(' ', '%20')}",
timeout=10,
)
r.raise_for_status()
data = r.json()
return data.get("text", "").strip()
except Exception:
return None
def _from_daily_plan() -> dict | None:
"""
Pick today's verse from the daily plan using day-of-year cycling.
The plan file (one reference per line) is read; today's entry is
determined by `day_of_year % len(plan)`. Both diatheke and the
`bibles` library are queried for each verse (to handle ranges),
and their results are concatenated side‑by‑side for display.
Returns:
A dict with keys:
- 'ref' : the original reference string (e.g. "Psalm 23:1-3")
- 'text' : a combined string showing diatheke and freebible output
- 'source': a description of the source
Returns None if the plan file is empty, the reference cannot be
parsed, or both lookups fail entirely.
"""
refs = _load_daily_plan(DAILY_PLAN_FILE)
if not refs:
return None
# day-of-year is 1‑based; we make it 0‑based for indexing
day_of_year = datetime.date.today().timetuple().tm_yday - 1
idx = day_of_year % len(refs)
ref = refs[idx]
parsed = _get_book_chapter_verses(ref)
if not parsed:
return None
book, chapter, verses = parsed
diatheke_texts = []
freebible_texts = []
for verse in verses:
t1 = _lookup_diatheke(book, chapter, verse)
t2 = _lookup_freebible(book, chapter, verse)
if t1:
diatheke_texts.append(t1)
if t2:
freebible_texts.append(t2)
# Offline sources failed → attempt bible‑api.com
fallback_text = None
if not diatheke_texts and not freebible_texts:
fallback_text = _lookup_bible_api(ref)
# If everything failed, give up
if not diatheke_texts and not freebible_texts and not fallback_text:
return None
# Build a combined text block showing each source separately
return {
"ref": ref,
"text":
(
"Diatheke:\n" + " ".join(diatheke_texts) + "\n\n"
if diatheke_texts else ""
)
+ (
"FreeBible:\n" + " ".join(freebible_texts) + "\n\n"
if freebible_texts else ""
)
+ (
"Bible API:\n" + fallback_text
if fallback_text else ""
),
"source":
f"KJV — daily plan day {idx + 1}/{len(refs)}; "
f"Sources: Diatheke + FreeBible + BibleAPI",
}
# ── Public entry point ────────────────────────────────────────────────────────
def verse_of_the_day(full: bool = False) -> None:
"""
Pick and print a Bible verse of the day using the available sources.
By default (`full=False`), the function tries sources in the following
order and stops at the first successful one:
1. YouVersion
2. Our Manna
3. Personal daily plan (with offline + online fallback)
If none work, a hardcoded fallback verse is printed.
When `full=True`, all available sources are printed one after another.
This function is designed to be called from a greeting script; it handles
all I/O and error recovery internally.
"""
sources = [
_from_youversion,
_from_ourmanna_api,
_from_daily_plan,
]
if not full:
for fn in sources:
if _print_verse(fn()):
return
# ultimate fallback
_print_verse(FALLBACK_VERSE)
return
# FULL MODE → print everything available
printed_any = False
for fn in sources:
if _print_verse(fn()):
printed_any = True
if not printed_any:
_print_verse(FALLBACK_VERSE)
def check_setup() -> None:
"""
Print a diagnostic about what is installed and configured.
Run this when setting up for the first time to verify what is installed
and whether the daily plan file is present and correctly formatted.
"""
print("Bible verse setup check:")
print(f" requests: {'✅ installed' if HAS_REQUESTS else '❌ missing — pip install requests'}")
print(f" freebible: {'✅ installed' if HAS_FREEBIBLE else '❌ missing — pip install freebible'}")
print(f" colorama: {'✅ installed' if HAS_COLOR else '⚠️ missing — pip install colorama (optional)'}")
print(f" daily plan: {'✅ found' if DAILY_PLAN_FILE.exists() else f'⚠️ not found — create {DAILY_PLAN_FILE}'}")
if DAILY_PLAN_FILE.exists():
refs = _load_daily_plan(DAILY_PLAN_FILE)
print(f" {len(refs)} references loaded")
day = datetime.date.today().timetuple().tm_yday - 1
today_ref = refs[day % len(refs)] if refs else "—"
print(f" Today's reference: {today_ref}")
# ── Script entry point ────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
if "--check" in sys.argv:
check_setup()
else:
verse_of_the_day()