# app.py from flask import Flask, request, jsonify, send_file import random from datetime import date # used for daily verse import sqlite3 import string # for literals import json # for dictionary import hunspell # for spell checking DEBUG = 0 """ To-do: Separate dictionary-dictionary for hunsepll Search phrase alias Response Bible book reading Search highlighting Styles Bible Reading Plan Bible Reader """ def dbg(message): if DEBUG >= 1: print(f"\033[91m{message}\033[0m") def dbg2(message): if DEBUG >= 2: print(f" \033[32m{message}\033[0m") def dbg3(message): if DEBUG >= 3: print(f" \033[35m{message}\033[0m") dbg("Debug Level 1 Enabled") dbg2("Debug Level 2 Enabled") dbg3("Debug Level 3 Enabled") dictionary_json = "data/1828_Webster_KJV.json" kjv_cur = sqlite3.connect("data/kjv.db").cursor() verse_ids = [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()] book_names = [i[0] for i in kjv_cur.execute("SELECT n FROM key_english").fetchall()] app = Flask(__name__) idx_id = 0 idx_book = 1 idx_chapter = 2 idx_verse = 3 idx_text = 4 def lookup_one_verse(verse_id): dbg2("lookup_one_verse({})".format(verse_id)) return kjv_cur.execute( "SELECT * FROM fts_kjv WHERE id = {};".format(verse_id) ).fetchone() def lookup_bookname(book_id): dbg3("lookup_bookname({})".format(book_id)) result = kjv_cur.execute( "SELECT n FROM key_english WHERE b = {};".format(book_id) ).fetchone() if result: result = result[0] dbg2("lookup_bookname.result = '{}'".format(result)) return result def lookup_book_id(bookname): dbg3("lookup_book_id({})".format(bookname)) result = kjv_cur.execute( "SELECT b FROM key_english WHERE n = '{}';".format(bookname) ).fetchone() if result: result = result[0] dbg3("lookup_book_id.result = '{}'".format(result)) return result def lookup_fts(kwds): dbg3("lookup_fts.kwds = '{}'".format(kwds)) return kjv_cur.execute("SELECT * FROM fts_kjv('{}')".format(kwds)).fetchall() def lookup_contains(kw): results = kjv_cur.execute( "SELECT * FROM fts_kjv WHERE t LIKE '%{}%';".format(kw) ).fetchall() dbg2("lookup_contains({}) {} results".format(kw, len(results))) return results def lookup_by_verse_id(start_verse, end_verse=None): if not end_verse: results = kjv_cur.execute( "SELECT * FROM fts_kjv WHERE id = {};".format(start_verse) ).fetchall() else: results = kjv_cur.execute( "SELECT * FROM fts_kjv WHERE id BETWEEN {} AND {};".format( start_verse, end_verse ) ).fetchall() return results def get_verse_id(book_id, chapter, verse): # verse_id is 00000000 (00 000 000) (book + chapter + verse) chapterid = chapter.zfill(3) verseid = verse.zfill(3) return str(book_id) + chapterid + verseid def check_lone_word(text, kw): kw_length = len(kw) kw_location = text.find(kw) try: if (text[kw_location - 1]).isalpha(): return False except IndexError: # If it's an indexerror, it's likely that # the keyword is at the end or beginning # of the text. pass try: if (text[kw_location + kw_length]).isalpha(): return False except IndexError: pass return True def lookup_contains_exact(kw): preresult = lookup_contains(kw) results = [] for result in preresult: if check_lone_word(result[idx_text], kw): results.append(result) dbg("lookup_contains({}) {} results".format(kw, len(results))) return results def ref_input_cleaning(ref, valid_chars=(string.ascii_letters + string.digits + ":-,")): dbg2("ref_input_cleaning({})".format(ref)) ref = ref.replace(" ", "") if ref.count(":") != 1: dbg2("ref_input_cleaning.if ref.count(:): TRUE") dbg2(" -> Assuming user wanted entire chapter") ref += ":-" if ref.count("-") > 1: dbg2("ref_input_cleaning.if ref.count(-): TRUE") return False if not ref[-1].isdigit() and ref[-1] != "-": dbg2("ref_input_cleaning.if ref[-1] TRUE") return False ref = list(ref) # preserve book number if we can guess the first # character is a number and 3nd is a letter if ref[0].isdigit() and ref[1].isalpha(): prefix = ref.pop(0) + " " else: prefix = "" # clean the rest of the reference cleaned_ref = [] for character in ref: if character in valid_chars: cleaned_ref.append(character) # Was there a problem? if len(cleaned_ref) == 0: return False return prefix + "".join(cleaned_ref).capitalize() def search_input_cleaning( query, valid_chars=string.ascii_letters + string.digits + '+^" ' ): cleaned_query = [] for character in query: if character in valid_chars: cleaned_query.append(character) return "".join(cleaned_query) def ref_parse_book(ref): for book in reversed(book_names): if book.startswith(ref): return book return False @app.get("/find") def parse_query(): query = request.args.get("kw", None) if not query: return send_file("html/find.html") result = show() if not hasattr(result, "__len__"): dbg3("parse_query.show = '{}'".format(result)) return result if len(result) > 17: return result result = "" if len(query.split()) == 1: result += "
" result += define() + "
" dbg3("parse_query.define = '{}'".format(result)) result += search() dbg3("parse_query.search = '{}'".format(result)) return result @app.get("/define") def define(): ref = request.args.get("kw", None) if ref: ref = ref.strip() if not ref: return send_file("html/dictionary.html") with open(dictionary_json, "r") as data: dictionary = json.load(data) ref = ref.lower() if not ref in dictionary.keys(): h = hunspell.HunSpell("data/kjv.dic", "data/kjv.aff") suggestions = h.suggest(ref) return "{} not found, try one of the following: {}".format( ref, ", ".join(suggestions) ) arg_view = request.args.get("view", None) if arg_view == "plain": response = return_dict_plain(ref, dictionary[ref]) elif arg_view == "rich": response = return_dict_rich(ref, dictionary[ref]) else: response = jsonify({ref: dictionary[ref]}) return response def return_dict_plain(ref, defs): response = "" for e in defs: response += e + "

" return ref.capitalize() + "

" + response + "\n" def return_dict_rich(ref, defs): header = open("html/header.html", "r") css = open("styles/human_readable.css", "r") response_html = open("html/response.html", "r") response = "" for e in defs: response += e + "

" # return (header.read() + response_html.read().format(passage = ref.capitalize(), text = response) + css.read()) return ( response_html.read().format(passage=ref.capitalize(), text=response) + css.read() ) @app.get("/show") def show(): ref = request.args.get("kw", None) if ref is None: return send_file("html/show.html") dbg("show({})".format(ref)) refs = [] for r in ref.split(","): r_ = ref_input_cleaning(r.strip()) if r_: refs.append(r_) if not refs: return "Invalid Query" verse_results = [] # check if the search is for multiple refs for ref in refs: dbg("show.reference({})".format(ref)) maybe_chapter = ref.split(":")[0][3:] ref_chapter = "" for maybedigit in maybe_chapter: if maybedigit.isdigit(): ref_chapter += maybedigit ref_verse = ref.split(":")[1] if ref.__contains__("-"): if ref.endswith("-"): start_verse = ref_verse.split("-")[0] end_verse = "500" # just get all the verses if it ends with a dash else: start_verse, end_verse = ( ref_verse.split("-")[0], ref_verse.split("-")[1], ) else: start_verse = ref_verse end_verse = None end_verse_id = None book_str = ref_input_cleaning(ref, string.ascii_letters) ref_bookname = ref_parse_book(book_str) ref_book_id = lookup_book_id(ref_bookname) if not ref_book_id: return "Invalid Query" start_verse_id = get_verse_id(ref_book_id, ref_chapter, start_verse) if end_verse: end_verse_id = get_verse_id(ref_book_id, ref_chapter, end_verse) dbg2("show.start_verse_id({})".format(start_verse_id)) dbg2("show.end_verse_id({})".format(end_verse_id)) results = lookup_by_verse_id(start_verse_id, end_verse_id) dbg3("show.results({})".format(results)) for result in results: verse_results.append(result) response_list = response_multi(verse_results) arg_view = request.args.get("view", None) if arg_view == "plain" or arg_view == "rich": response = "
".join(response_list) else: response = jsonify(response_list) return response def response_dict(scripture): # used for JSON responses verse = { "bookname": lookup_bookname(scripture[idx_book]), "chapter": scripture[idx_chapter], "verse": scripture[idx_verse], "text": scripture[idx_text], } return verse def response_text(scripture): bookname = lookup_bookname(scripture[idx_book]) chapter = str(scripture[idx_chapter]) verse = str(scripture[idx_verse]) text = str(scripture[idx_text]) return bookname + " " + chapter + ":" + verse + " " + text + "\n" def response_rich(scripture): css = open("styles/human_readable.css", "r") response_html = open("html/response.html", "r") # (passage, text) is the html formatting arguments bookname = lookup_bookname(scripture[idx_book]) chapter = str(scripture[idx_chapter]) verse = str(scripture[idx_verse]) text = str(scripture[idx_text]) passage = bookname + " " + chapter + ":" + verse return response_html.read().format(passage=passage, text=text) + css.read() def response_single(scripture): dbg2("response_single({})".format(scripture)) arg_view = request.args.get("view", "json") dbg("->requested_format = {}".format(arg_view)) if arg_view == "plain": return response_text(scripture) elif arg_view == "rich": header = open("html/header.html", "r") return header.read() + response_rich(scripture) else: response = response_dict(scripture) return jsonify(response) def response_multi(scriptures): response = [] arg_view = request.args.get("view", "json") dbg("search ->requested_format = {}".format(arg_view)) if arg_view == "plain": for scripture in scriptures: response.append(response_text(scripture)) elif arg_view == "rich": response.append(open("html/header.html", "r").read()) for scripture in scriptures: response.append(response_rich(scripture)) else: for scripture in scriptures: response.append(response_dict(scripture)) return response @app.errorhandler(404) def page_not_found(e): message = { "error": "You're lost! Isaiah 41:10 - Fear thou not; for I am with thee: be not dismayed; for I am thy God: I will strengthen thee; yea, I will help thee; yea, I will uphold thee with the right hand of my righteousness." } return jsonify(message) @app.get("/") def idx(): return send_file("html/index.html") @app.get("/salvation") def salvation(): return send_file("html/heaven.html") # backwards-compatibility @app.get("/t/random", defaults={"view": "plain"}) @app.get("/random") def random_verse(): rand_id = random.SystemRandom().choice(verse_ids) scripture = lookup_one_verse(rand_id) return response_single(scripture) # backwards-compatibility @app.get("/t/votd", defaults={"view": "plain"}) @app.get("/votd") def verse_of_the_day(): today = int(str(date.today()).replace("-", "")) # this is deterministic because of today's date being used as the seed random.seed(today) scripture = lookup_one_verse(random.choice(verse_ids)) return response_single(scripture) @app.get("/search") def search(): keywords = request.args.get("kw", None) dbg("search({})".format(keywords)) if keywords: cleaned_keywords = search_input_cleaning(keywords) if not cleaned_keywords: return send_file("html/search.html") else: return send_file("html/search.html") results = lookup_fts(cleaned_keywords) if not results: h = hunspell.HunSpell("data/kjv.dic", "data/kjv.aff") suggestions = h.suggest(cleaned_keywords) return "{} not found, try one of the following: {}".format( cleaned_keywords, ", ".join(suggestions) ) response_list = response_multi(results) arg_view = request.args.get("view", None) if arg_view == "plain" or arg_view == "rich": response = "
".join(response_list) else: response = jsonify(response_list) dbg2("Search Response: {}".format(response)) return response @app.get("/reader") def bible_reader(): return send_file("html/reader.html") # We should return an index of all books with # drop down menus of each chapter, and a search # bar at the top for quick seeking, which can use # show(). Each page should return a header and footer. # header should have a navigation bar and "next/previous chapter" # Footer should have next/previous as well. # Maybe preliminary theming support @app.get("/plan") def reading_plan(): return send_file("html/reading_plan.html") # needs 2 sets of data, books and time frame. # should default to the entire Bible in 1 year. # should start today() and end today()+1year # Should return a calender with date and verse range.