# 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 = 2 """ To-do: Styles Bible Reading Plan Bible Viewer """ def dbg(message): # if DEBUG is at or above a value of one, a call from debug level one will print red if DEBUG >= 1: print(f'\033[91m{message}\033[0m') def dbg2(message): # if DEBUG is at or above a value of two, a call from debug level one will print green if DEBUG >= 2: print(f'\033[32m{message}\033[0m') dbg("Debug Level 1 Enabled") dbg2("Debug Level 2 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): dbg("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): dbg2("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] dbg("lookup_bookname.result = '{}'".format(result)) return result def lookup_book_id(bookname): dbg2("lookup_book_id({})".format(bookname)) result = kjv_cur.execute("SELECT b FROM key_english WHERE n = '{}';".format(bookname)).fetchone() if result: result = result[0] dbg2("lookup_book_id.result = '{}'".format(result)) return result def lookup_fts(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() dbg("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 = list(ref.replace(" ","")) if ref.count(':') != 1: dbg2("ref_input_cleaning.if ref.count(:): TRUE") return False 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 # 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("/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 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()) @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 ref: 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("Book reference invalid. Ex: 1John3:16") 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) dbg2("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) 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