Massive code refactoring, and some test rewrites.

This commit is contained in:
Mike Young
2019-11-15 14:45:42 -05:00
parent e012436011
commit d7279a73a7
127 changed files with 3960 additions and 345 deletions

4
.gitignore vendored Normal file → Executable file
View File

@@ -1,5 +1,5 @@
app/books/* books/*
*.json *.idea
*.pyc *.pyc
app/content.opf app/content.opf
.vscode .vscode

0
.pre-commit-config.yaml Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

4
__init__.py Executable file
View File

@@ -0,0 +1,4 @@
import sys
sys.path.insert(1, 'app/')
sys.path.insert(2, 'frontend/')

View File

@@ -1,4 +0,0 @@
import os
import sys
sys.path.insert(0, os.path.abspath('.'))

View File

@@ -1,15 +0,0 @@
class Config:
"""Main System Configuration"""
def __init__(self):
self.book_path = "books/"
self.TITLE = "pyShelf E-Book Server"
self.VERSION = "0.1.0"
self.TITLE = self.TITLE + " ver" + self.VERSION
self.book_shelf = "data/shelf.json"
# self.catalogue_db = "data/catalogue.db"
self.catalogue_db = "../frontend/db.sqlite3"
self.file_array = [
self.book_shelf,
self.catalogue_db,
]
self.auto_scan = True

1
app/data/shelf.json Executable file

File diff suppressed because one or more lines are too long

0
app/lib/__init__.py Normal file → Executable file
View File

0
app/lib/api_hooks.py Normal file → Executable file
View File

31
app/lib/config.py Executable file
View File

@@ -0,0 +1,31 @@
import json
import os
import sys
class Config:
"""
Main System Configuration
"""
_fp = "config.json"
print(os.path)
def __init__(self, root=os.path.abspath('../')):
_data = self.open_file(root)
self.book_path = _data['BOOKPATH']
self.TITLE = _data['TITLE']
self.VERSION = _data['VERSION']
self.TITLE = self.TITLE + " ver " + self.VERSION
self.book_shelf = _data['BOOKSHELF']
# self.catalogue_db = "data/catalogue.db"
self.catalogue_db = root+'/'+_data['DATABASE']
self.file_array = [
self.book_shelf,
self.catalogue_db,
]
self.auto_scan = True
def open_file(self, root):
with open(root+'/'+self._fp, "r") as read_file:
data = json.load(read_file)
return data

View File

@@ -1,103 +0,0 @@
#!/usr/bin/python
import cgi
import sys
from config import Config
sys.path.insert(0, '../')
class Frontend():
"""Dynamic frontend display functions"""
def __init__(self, dimensions=[0, 0]):
"""
:param dimensions: array containing screen size [x, y]
"""
self.dimensions = dimensions
self.TITLE = Config().TITLE
def html_Headers(self):
"""
HTML headers
:returns _head: HTML render of page headers
"""
_head = """
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta http-equiv=\"X-UA-Compatible\" content=\"IE-edge\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<link type=\"text/css\" rel=\"stylesheet\" href=\"/css/main.css\" />
<title>%s</title>
</head>
""" % self.TITLE
return _head
def app_Headers(self):
"""
App specific headers
:returns _head: HTML render of application specific headers
"""
_head = """
<body>
<div id=\"app\">
<div class=\"app_header\">
<h1 class=\"app_hdr shadow\">pyShelf</h1>
<h2> class=\"app_subhdr shadow\">Open Source E-book Server</h2>
</div>
"""
return _head
def app_body(self, nav, shelf):
"""
Main interface body, and navigation
:param nav: nav[] system navigation list
:param shelf: shelf[0{path:"",title:"",cover:"",author:""}]
:returns _body: HTML render of page body
"""
_body = """
<div class=\"app_body\">
<div class=\"left_col\">
%s
</div>
<div class=\"shelf\">
<div class=\"shelf_contents\">
%s
</div>
</div>
</div>
""" %(nav, shelf)
return _body
def app_footer(self):
"""
Main interface footer; Closes HTML
:returns _footer: HTML render of page footer
"""
_footer = """
<div class=\"app_footer\">
<div class=\"python_logo\">
<img src=\"/img/py.png\" id=\"python_logo\" />
</div>
</div>
</div>
</body>
</html>
"""
return _footer
def compile(self, nav, shelf):
"""
Compiles user interface
:returns _ui: Compiled HTML for page layout
"""
_head = self.html_Headers() + self.app_Headers()
_body = self.app_body(nav, shelf)
_foot = self.app_footer()
try:
_ui = _head + _body + _foot
return _ui
except Exception as e:
return e

View File

@@ -7,39 +7,42 @@ import zipfile
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from PIL import Image from PIL import Image
from config import Config from .api_hooks import DuckDuckGo
from lib.api_hooks import DuckDuckGo from .config import Config
from lib.storage import Storage from .storage import Storage
config = Config() # config = Config()
class Catalogue: class Catalogue:
"""Decodes and stores book information""" """Decodes and stores book information"""
"""Step One: filter_books""" """Step One: filter_books"""
def __init__(self): def __init__(self, root):
self.file_list = [] self.file_list = []
self.opf_regx = re.compile(r'\.opf') self.opf_regx = re.compile(r'\.opf')
self.cover_regx = re.compile(r'\.jpg|\.jpeg|\.png|\.bmp|\.gif') self.cover_regx = re.compile(r'\.jpg|\.jpeg|\.png|\.bmp|\.gif')
self.html_regx = re.compile(r'\.html') self.html_regx = re.compile(r'\.html')
self.root_dir = root
_config = Config(root)
self.book_folder = _config.book_path
self.book_shelf = _config.book_shelf
self._book_list_expanded = None
self.books = None
def scan_folder(self, folder=config.book_path): def scan_folder(self, _path=None):
if _path is not None:
folder = _path
elif os.path.isdir(self.root_dir+'/'+self.book_folder):
folder = self.root_dir+'/'+self.book_folder
else: folder = self.book_folder
for f in os.listdir(folder): for f in os.listdir(folder):
_path = os.path.abspath(folder+'/'+f) _path = os.path.abspath(folder+'/'+f)
#_path = os.path.abspath('.')+'/'+folder+f+'/'
_is_dir = os.path.isdir(_path.strip()+'/') _is_dir = os.path.isdir(_path.strip()+'/')
if _is_dir: if _is_dir:
self.file_list.append(self.scan_folder(_path)) self.file_list.append(self.scan_folder(_path))
self.file_list.append(_path) self.file_list.append(_path)
def scan_book(self, book):
"""REMOVE ME?"""
_epub = zipfile.ZipFile(book)
with _epub as _epub_open:
try: _epub_open.open('content.opf'); return True
except Exception as e: print(e); return False
def filter_books(self): def filter_books(self):
""" """
Scan book folder recursively for epub files Scan book folder recursively for epub files
@@ -50,17 +53,20 @@ class Catalogue:
""" """
self.scan_folder() self.scan_folder()
regx = re.compile(r"\.epub") regx = re.compile(r"\.epub")
self.books = list(filter(regx.search, filter(None, self.file_list))) try:
_book_list_expanded = {} self.books = list(filter(regx.search, filter(None, self.file_list)))
with open(config.book_shelf, 'w') as f: except TypeError as e:
print(e)
self._book_list_expanded = {}
with open(self.book_shelf, 'w') as f:
for book in self.books: for book in self.books:
_book_list_expanded[book] = self.process_book(book) self._book_list_expanded[book] = self.process_book(book)
json.dump(_book_list_expanded, f) json.dump(self._book_list_expanded, f)
return _book_list_expanded return self._book_list_expanded
def process_book(self, book): @staticmethod
def process_book(book):
"""Return dictionary of epub file contents""" """Return dictionary of epub file contents"""
f_name = 'content.opf'
book = zipfile.ZipFile(book, 'r') book = zipfile.ZipFile(book, 'r')
details = {} details = {}
with book as book_zip: with book as book_zip:
@@ -86,11 +92,11 @@ class Catalogue:
content = self.extract_content(book_zip, book) content = self.extract_content(book_zip, book)
soup = BeautifulSoup(content, "lxml") soup = BeautifulSoup(content, "lxml")
title = soup.find("dc:title") title = soup.find("dc:title")
if title == None: if title is None:
title = book['path'].split('/')[-1].rsplit('.', 1)[0] title = book['path'].split('/')[-1].rsplit('.', 1)[0]
else: title = title.contents[0] else: title = title.contents[0]
author = soup.find("dc:creator") author = soup.find("dc:creator")
if author != None: author = author.contents[0] if author is not None: author = author.contents[0]
try: cover = self.extract_cover_image(book_zip, book) try: cover = self.extract_cover_image(book_zip, book)
except IndexError: except IndexError:
# cover = self.extract_cover_html(book_zip, book) # cover = self.extract_cover_html(book_zip, book)
@@ -127,8 +133,8 @@ class Catalogue:
db = Storage() db = Storage()
stored = db.book_paths_list() stored = db.book_paths_list()
closed = db.close() closed = db.close()
try: self.books if self.books is None:
except Exception: self.filter_books() self.filter_books()
on_disk, in_storage = [], [] on_disk, in_storage = [], []
for _x in self.books: on_disk.append(_x) for _x in self.books: on_disk.append(_x)
for _y in stored: in_storage.append(_y[0]) for _y in stored: in_storage.append(_y[0])

View File

@@ -1,15 +1,11 @@
#!/usr/bin/python #!/usr/bin/python
import mimetypes
import os import os
import zipfile
from http.server import BaseHTTPRequestHandler, HTTPServer
from config import Config from .config import Config
from lib.library import Catalogue from .storage import Storage
from lib.storage import Storage
config = Config() # config = Config()
Storage = Storage() # Storage = Storage()
class InitFiles: class InitFiles:
@@ -29,47 +25,10 @@ class InitFiles:
f.close() f.close()
class RequestHandler(BaseHTTPRequestHandler):
"""Request Handler"""
def do_GET(self):
# TODO determine how to include stylesheets
self.send_response(200)
if self.path == '/':
self.path = '../static/index.html'
mimetype = 'text/html'
serve_file = open(self.path[1:]).read()
self.send_header('Content-type', mimetype)
self.end_headers()
self.wfile.write(bytes(serve_file, 'utf-8'))
elif self.path.split('.', 1)[1] == 'css':
self.path = '../static' + self.path
mimetype = 'text/css'
serve_file = open(self.path[1:]).read()
self.send_header('Content-type', mimetype)
self.end_headers()
self.wfile.write(bytes(serve_file, 'utf-8'))
elif self.path.endswith('.png'):
self.path = '../static' + self.path
mimetype = 'image/png'
serve_file = open(self.path[1:], 'rb')
# Important to rb read binary for images
self.send_header('Content-type', mimetype)
self.end_headers()
self.wfile.write(serve_file.read())
else:
self.send_response(404)
serve_file = "File Not Found"
mimetype = 'text/html'
self.send_header('Content-type', mimetype)
self.end_headers()
try: serve_file.close()
except Exception: pass
class BookDisplay: class BookDisplay:
"""All functions related to displaying book information in the HTML UI""" """All functions related to displaying book information in the HTML UI"""
def __init__(self): def __init__(self, **kwargs):
""" """
Initialize class variables Initialize class variables
:return: None :return: None
@@ -79,6 +38,8 @@ class BookDisplay:
self.thumbnail_size = [200, 300] self.thumbnail_size = [200, 300]
self.thumbnail_scale = 1 self.thumbnail_scale = 1
self.total_pages = None self.total_pages = None
try: self.screen_size = kwargs['screen_size']
except Exception: self.screen_size = [900, 600]
def nextPage(self): def nextPage(self):
""" """
@@ -105,45 +66,4 @@ class BookDisplay:
""" """
x = (self.thumbnail_size[0] * self.thumbnail_scale) + 10 x = (self.thumbnail_size[0] * self.thumbnail_scale) + 10
y = (self.thumbnail_size[1] * self.thumbnail_scale) + 10 y = (self.thumbnail_size[1] * self.thumbnail_scale) + 10
self.books_per_page = int(screen_size[0]//x) * int(screen_size[1]//y) self.books_per_page = int(self.screen_size[0]//x) * int(self.screen_size[1]//y)
return self.books_per_page
class BookServer:
"""
HTTP server functions required to display e-books
"""
def __init__(self):
self.server_address = ('', 8000)
self.handler = RequestHandler
self.httpd = HTTPServer(self.server_address, self.handler)
def close_prompt(self):
"""Prompt to close server"""
close = input("Close Server? y/n")
if close == 'y':
self.close()
return True
else:
self.close_prompt()
def run(self):
"""Start HTTP Server"""
try:
print("Server running @ http://127.0.0.1:8000")
self.httpd.serve_forever()
self.httpd.handle_request()
except KeyboardInterrupt:
print("Interrupt received, Closing Server")
self.close()
print("Server shutdown, Goodbye!")
return True
def close(self):
"""Stop HTTP Server"""
try:
self.httpd.server_close()
return True
except Exception:
return False

14
app/lib/storage.py Normal file → Executable file
View File

@@ -3,18 +3,20 @@ import sqlite3
import sys import sys
# sys.path.insert(1, '../') # sys.path.insert(1, '../')
from config import Config from .config import Config
db_pointer = Config().catalogue_db # db_pointer = Config().catalogue_db
class Storage: class Storage():
"""Contains all methods for system storage""" """Contains all methods for system storage"""
def __init__(self): def __init__(self, db_pointer=None):
# Optionaly pass db_file to specify another db or for testing
if db_pointer is None: db_pointer = Config().catalogue_db
self.db_file = db_pointer self.db_file = db_pointer
self.database() self.database()
self.create_tables() # self.create_tables()
def database(self): def database(self):
"""Create database cursor""" """Create database cursor"""
@@ -23,6 +25,8 @@ class Storage:
self.cursor = self.db.cursor() self.cursor = self.db.cursor()
return True return True
except Exception as e: except Exception as e:
print(self.db_file)
print(e)
return False return False
def create_tables(self): def create_tables(self):

View File

@@ -1,21 +1,14 @@
#!/usr/bin/python #!/usr/bin/python
import os
import sys import sys
from config import Config from lib.config import Config
from lib.display import Frontend
from lib.library import Catalogue from lib.library import Catalogue
from lib.pyShelf import BookDisplay, BookServer, InitFiles from lib.pyShelf import InitFiles
# sys.path.insert(1, 'lib/') ROOT_DIR = os.path.abspath('../')
sys.path.append(ROOT_DIR)
config = Config() # Get configuration settings config = Config(ROOT_DIR) # Get configuration settings
InitFiles(config.file_array) # Initialize file system InitFiles(config.file_array) # Initialize file system
Catalogue = Catalogue() # Open the Catalogue Catalogue = Catalogue(ROOT_DIR) # Open the Catalogue
UI = Frontend()
Server = BookServer()
# new_books = Catalogue.new_files()
Catalogue.import_books() # Filter Your books Catalogue.import_books() # Filter Your books
# Server.run()
# TODO Figure out a system to get books page count
# TODO Update Documentation
# TODO Requirements.txt

0
app/pyproject.toml Normal file → Executable file
View File

0
app/static/css/main.css Normal file → Executable file
View File

0
app/static/index.html Normal file → Executable file
View File

0
app/tests/__init__.py Executable file
View File

16
app/tests/config_test.py Executable file
View File

@@ -0,0 +1,16 @@
import os
from ..lib.config import Config
class TestConfig:
config = Config(os.path.abspath(os.path.curdir))
def test_book_dir(self):
assert os.path.isdir(self.config.book_path)
def test_title(self):
assert 'pyShelf' in self.config.TITLE
def test_version(self):
assert self.config.VERSION is not None

35
app/tests/library_test.py Executable file
View File

@@ -0,0 +1,35 @@
import json
import os
from ..lib.config import Config
from ..lib.library import Catalogue
class Test_Config(Config):
def __init__(self):
Config.__init__(self, 'config.json')
_data = self.open_file()
def open_file(self, root='config.json'):
with open('config.json') as read_file:
data = json.load(read_file)
return data
class Test_Catalogue(Catalogue):
def __init__(self):
Catalogue.__init__(self, root=os.path.abspath('.'))
def filter_books(self):
self.book_shelf = 'app/'+self.book_shelf
return super().filter_books()
class TestCatalogue:
root = os.path.abspath(os.path.curdir)
config = Test_Config()
def test_filter_books(self):
book_list = Test_Catalogue().filter_books()
assert len(book_list) > 0

7
config.json Normal file
View File

@@ -0,0 +1,7 @@
{
"TITLE": "pyShelf E-Book Server",
"VERSION": "0.1.0",
"BOOKPATH": "books/",
"DATABASE": "frontend/db.sqlite3",
"BOOKSHELF": "data/shelf.json"
}

0
docs/html/HTML/D/index.html Normal file → Executable file
View File

0
docs/html/HTML/FILEMAP Normal file → Executable file
View File

0
docs/html/HTML/GTAGSROOT Normal file → Executable file
View File

0
docs/html/HTML/I/index.html Normal file → Executable file
View File

0
docs/html/HTML/J/index.html Normal file → Executable file
View File

0
docs/html/HTML/R/index.html Normal file → Executable file
View File

0
docs/html/HTML/S/index.html Normal file → Executable file
View File

0
docs/html/HTML/Y/index.html Normal file → Executable file
View File

0
docs/html/HTML/defines.html Normal file → Executable file
View File

0
docs/html/HTML/defines/index.html Normal file → Executable file
View File

0
docs/html/HTML/files.html Normal file → Executable file
View File

0
docs/html/HTML/files/index.html Normal file → Executable file
View File

0
docs/html/HTML/help.html Normal file → Executable file
View File

0
docs/html/HTML/index.html Normal file → Executable file
View File

0
docs/html/HTML/mains.html Normal file → Executable file
View File

0
docs/html/HTML/rebuild.sh Normal file → Executable file
View File

0
docs/html/annotated.html Normal file → Executable file
View File

0
docs/html/bc_s.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 676 B

0
docs/html/bdwn.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 147 B

After

Width:  |  Height:  |  Size: 147 B

0
docs/html/classapp_1_1config_1_1Config-members.html Normal file → Executable file
View File

0
docs/html/classapp_1_1config_1_1Config.html Normal file → Executable file
View File

View File

View File

View File

0
docs/html/classapp_1_1lib_1_1display_1_1Frontend.html Normal file → Executable file
View File

View File

0
docs/html/classapp_1_1lib_1_1library_1_1Catalogue.html Normal file → Executable file
View File

View File

View File

View File

View File

View File

0
docs/html/classapp_1_1lib_1_1pyShelf_1_1InitFiles.html Normal file → Executable file
View File

View File

View File

View File

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

View File

0
docs/html/classapp_1_1lib_1_1storage_1_1Storage.html Normal file → Executable file
View File

0
docs/html/classes.html Normal file → Executable file
View File

0
docs/html/closed.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 132 B

0
docs/html/dir_9dc6c7acf21934bbaaf79b41db58c4e7.html Normal file → Executable file
View File

0
docs/html/dir_d422163b96683743ed3963d4aac17747.html Normal file → Executable file
View File

0
docs/html/doc.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 746 B

After

Width:  |  Height:  |  Size: 746 B

0
docs/html/doxygen.css Normal file → Executable file
View File

0
docs/html/doxygen.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

0
docs/html/dynsections.js Normal file → Executable file
View File

0
docs/html/files.html Normal file → Executable file
View File

0
docs/html/folderclosed.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 616 B

After

Width:  |  Height:  |  Size: 616 B

0
docs/html/folderopen.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 597 B

0
docs/html/functions.html Normal file → Executable file
View File

0
docs/html/functions_func.html Normal file → Executable file
View File

0
docs/html/hierarchy.html Normal file → Executable file
View File

0
docs/html/index.hhc Normal file → Executable file
View File

0
docs/html/index.hhk Normal file → Executable file
View File

0
docs/html/index.hhp Normal file → Executable file
View File

0
docs/html/index.html Normal file → Executable file
View File

0
docs/html/jquery.js vendored Normal file → Executable file
View File

0
docs/html/menu.js Normal file → Executable file
View File

0
docs/html/menudata.js Normal file → Executable file
View File

0
docs/html/nav_f.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 153 B

0
docs/html/nav_g.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 95 B

0
docs/html/nav_h.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 98 B

After

Width:  |  Height:  |  Size: 98 B

0
docs/html/open.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

0
docs/html/splitbar.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

0
docs/html/sync_off.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 853 B

After

Width:  |  Height:  |  Size: 853 B

0
docs/html/sync_on.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 845 B

0
docs/html/tab_a.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 142 B

After

Width:  |  Height:  |  Size: 142 B

0
docs/html/tab_b.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

0
docs/html/tab_h.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

0
docs/html/tab_s.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

0
docs/html/tabs.css Normal file → Executable file
View File

0
docs/man/man3/app_config_Config.3 Normal file → Executable file
View File

0
docs/man/man3/app_lib_api_hooks_DuckDuckGo.3 Normal file → Executable file
View File

0
docs/man/man3/app_lib_display_Frontend.3 Normal file → Executable file
View File

0
docs/man/man3/app_lib_library_Catalogue.3 Normal file → Executable file
View File

0
docs/man/man3/app_lib_pyShelf_BookDisplay.3 Normal file → Executable file
View File

0
docs/man/man3/app_lib_pyShelf_BookServer.3 Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More