diff --git a/Pipfile b/Pipfile index e60e742..05eeb84 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,9 @@ lxml = "*" sqlalchemy = "==2.0.0b3" pre-commit = "*" fastapi = {extras = ["all"], version = "*"} +debugpy = "*" +pudb = "*" +jinja2 = "*" [dev-packages] diff --git a/configure b/configure index 8d18074..86968f1 100755 --- a/configure +++ b/configure @@ -4,7 +4,9 @@ import json from src.backend.lib.storage import Storage from src.backend.lib.config import Config + def load_config(): + """Load program configuration.""" with open('config.json', "r") as file: config = json.load(file) file.close() @@ -12,20 +14,14 @@ def load_config(): def write_config(config): + """Write program configuration.""" with open('config.json', "w") as file: json.dump(config, file) file.close() -def set_secret(config=load_config()): - if config["SECRET"] == "": - config["SECRET"] = get_random_secret_key() - print(config["SECRET"]) - else: - print("Secret already set, skipping.") - - def set_book_directory(config=load_config(), *args): + """Set book directory.""" if config["BOOKPATH"] == "": try: config["BOOKPATH"] = args[0] @@ -33,26 +29,9 @@ def set_book_directory(config=load_config(), *args): config["BOOKPATH"] = input("Input Book Directory ") -def init_django_database(): - cmds = [ - 'python3 manage.py makemigrations', - 'python3 manage.py makemigrations interface', - 'python3 manage.py migrate', - 'python3 manage.py migrate interface', - ] - os.chdir("src") - for cmd in cmds: - os.system(cmd) - os.chdir("../") - - config_file = load_config() config = Config(os.path.split(os.path.realpath(__file__))[0]) -set_secret(config_file) set_book_directory(config_file) write_config(config_file) -# TODO:: Refactor here to enable backend to handle database operations. storage = Storage(config) storage.create_tables() -# init_django_database() -# Admin(Path.cwd()).createsuperuser() diff --git a/pyShelf.py b/pyShelf.py index b8b3caf..0cb1880 100755 --- a/pyShelf.py +++ b/pyShelf.py @@ -6,14 +6,15 @@ from pathlib import Path from threading import Thread import uvicorn -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.routing import APIRoute +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from src.backend.lib.config import Config from src.backend.pyShelf_MakeCollections import MakeCollections from src.backend.pyShelf_ScanLibrary import execute_scan - # import websockets @@ -22,7 +23,8 @@ config = Config(root) PRG_PATH = Path.cwd().__str__() sys.path.insert(0, PRG_PATH) app = FastAPI() - +app.mount("/static", StaticFiles(directory="src/frontend/static"), name="static") +templates = Jinja2Templates(directory="src/frontend/templates") def RunImport(): """Begin live import of books.""" @@ -34,51 +36,54 @@ def RunImport(): def use_route_names_as_operation_ids(app: FastAPI) -> None: + """Use route name as operation id.""" for route in app.routes: if isinstance(route, APIRoute): route.operation_id = route.name @app.get("/", response_class=HTMLResponse) -async def index(): - return """ - - - pyShelf eBook Server - - -

pyShelf Open Source Content Server

- - - """ +async def index(request: Request): + """Home page responder.""" + return templates.TemplateResponse( + "index.html", + {"request": request}) @app.get("/users/me") async def about_me(): + """About me page responder.""" return {"user_id": "CurrentUser"} @app.get("/users/{user_id}") async def about_user(user_id: int): + """About user page responder.""" return {"user_id": user_id} @app.get("/dev/test/echo/{_test_item_}") async def echo_test(_test_item_): + """Test echo responder function.""" return {"Test Object": _test_item_} async def fe_server(): + """Front end server entrypoint.""" config.logger.info("Starting FastAPI server.") - fe_config = uvicorn.Config("__main__:app", port=8080, log_level="info", reload=True) + fe_config = uvicorn.Config("__main__:app", port=8080, + log_level="info", reload=True) fe_server = uvicorn.Server(fe_config) await fe_server.serve() async def main(): + """Program entrypoint.""" _import_thread = Thread(target=RunImport) _import_thread.start() - asyncio.create_task(fe_server()) + _task = await asyncio.create_task(fe_server()) + breakpoint() + return [_task, _import_thread] if __name__ == "__main__": @@ -87,8 +92,8 @@ if __name__ == "__main__": loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() - loop.create_task(main()) - loop.run_forever() + _main_task = loop.create_task(main()) + loop.run_until_complete() loop.close() - exit - # asyncio.get_event_loop(asyncio.run(main())).run_forever() + loop.shutdown_default_executor() + exit(0) diff --git a/src/backend/lib/config.py b/src/backend/lib/config.py index 23c8fad..261f1d4 100755 --- a/src/backend/lib/config.py +++ b/src/backend/lib/config.py @@ -1,18 +1,15 @@ +"""Pyshelf's Configuration Object.""" import json import pathlib -import re import os from loguru import logger class Config: - """ - Main System Configuration - """ + """Main System Configuration.""" + def __init__(self, root): - """ - Initialize main configuration options - """ + """Initialize main configuration options.""" self.root = root env = os.environ.copy() self._fp = "config.json" @@ -37,13 +34,15 @@ class Config: self.db_port = env.get("DB_PORT", self._data["DB_PORT"]) self.file_array = [self.book_shelf] self.auto_scan = True - self.allowed_hosts = env.get("ALLOWED_HOSTS", self._data["ALLOWED_HOSTS"]) + self.allowed_hosts = env.get("ALLOWED_HOSTS", + self._data["ALLOWED_HOSTS"]) + self.db_engine = env.get("DB_ENGINE", self._data["DB_ENGINE"]) self.db_user = env.get("USER", self._data["USER"]) self.db_pass = env.get("PASSWORD", self._data["PASSWORD"]) - self.SECRET = env.get("SECRET", self._data["SECRET"]) self.build_mode = env.get("BUILD_MODE", self._data["BUILD_MODE"]) def get_logger(self): + """Instantiate logging system.""" _logger = logger _logger.add(pathlib.PurePath(self.root, 'data', 'pyshelf.log'), rotation="2 MB", @@ -52,17 +51,7 @@ class Config: return _logger def open_file(self): - """ - Opens config.json and reads in configuration options - """ + """Open config.json and reads in configuration options.""" with open(str(self._cp), "r") as read_file: data = json.load(read_file) return data - - def path(self): - rstr = "pyShelf/src" - r = re.template(rstr) - _pathre = re.match("pyShelf/src") - - def django_secret(self): - pass diff --git a/src/backend/lib/library.py b/src/backend/lib/library.py index 23f6014..245242f 100644 --- a/src/backend/lib/library.py +++ b/src/backend/lib/library.py @@ -40,35 +40,41 @@ class Catalogue: folder = str(self.root_dir) + "/" + self.book_folder else: folder = self.book_folder - for f in os.listdir(folder): - _path = os.path.abspath(folder + "/" + f) - if os.path.isdir(_path.strip() + "/"): - self.file_list.append(self.scan_folder(_path)) - else: - self.file_list.append(_path) + try: + for f in os.listdir(folder): + _path = os.path.abspath(folder + "/" + f) + if os.path.isdir(_path.strip() + "/"): + self.file_list.append(self.scan_folder(_path)) + else: + self.file_list.append(_path) + except FileNotFoundError as fnfe: + self.config.logger.error(fnfe) def filter_books(self): - """ - Calls scan_folder and filters out book files - Proceeds to call process_book + """Calls scan_folder and filters out book files. - :returns self._book_list_expanded: json string containing all book metadata + :returns self._book_list_expanded: json string containing + all book metadata """ self.scan_folder() # Populate file list regx = re.compile(r"\.epub|\.mobi|\.pdf") try: - self.books = list(filter(regx.search, filter(None, self.file_list))) - except TypeError as e: - self.config.logger.error(e) + self.books = list(filter( + regx.search, filter(None, self.file_list))) + except TypeError as error: + self.config.logger.error(error) + def process_by_filetype(self, book): + """Determine books filetype and process.""" if book.endswith(".epub"): epub = self.process_epub(book) return self.extract_metadata_epub(epub) - elif book.endswith(".mobi"): + if book.endswith(".mobi"): return self.extract_metadata_mobi(book) - elif book.endswith(".pdf"): + if book.endswith(".pdf"): return self.extract_metadata_pdf(book) + self.config.logger.error(f"Unknown Filetype {book}") @staticmethod def process_epub(book): diff --git a/src/backend/lib/storage.py b/src/backend/lib/storage.py index 1490ab4..951045d 100644 --- a/src/backend/lib/storage.py +++ b/src/backend/lib/storage.py @@ -1,6 +1,6 @@ -#!/usr/bin/python +"""Pyshelf's Main Storage Class.""" import re - +import os from sqlalchemy import create_engine, select from sqlalchemy.orm import Session @@ -8,27 +8,47 @@ from .models import Book, Collection class Storage: - """Contains all methods for system storage""" + """Contains all methods for system storage.""" def __init__(self, config): - self.sql = config.catalogue_db - self.user = config.user - self.password = config.password - self.db_host = config.db_host - self.db_port = config.db_port - self.engine = create_engine( - f"postgresql://{self.user}:{self.password}@{self.db_host}:{self.db_port}/{self.sql}" - ) + """Initialize storage object.""" self.config = config + self.sql = self.config.catalogue_db + self.user = self.config.user + self.password = self.config.password + self.db_host = self.config.db_host + self.db_port = self.config.db_port + self.engine = create_engine(self.get_connection_string(), + pool_pre_ping=True) + + def get_connection_string(self): + """Get connection string. + + Engine type references config.json:DB_ENGINE. + """ + if self.config.db_engine == "sqlite": + if os.path.exists(f"{self.config.root}/pyshelf.db"): + return f"sqlite:////{self.config.root}/pyshelf.db" + else: + sqlite_file = open(f'{self.config.root}/pyshelf.db', 'w') + sqlite_file.close() + return f"sqlite://{self.config.root}/pyshelf.db" + elif self.config.db_engine == "psql": + return f"postgresql://{self.user}:{self.password}\ + @{self.db_host}:{self.db_port}/{self.sql}" + elif self.config.db_engine == "mysql": + return f"mysql://{self.user}:{self.password}\ + @{self.db_host}:{self.db_port}/{self.sql}" def create_tables(self): + """Create table structure.""" tables = [Book, Collection] for table in tables: table.metadata.create_all(self.engine) def insert_book(self, book): - """ - Insert book in database + """Insert a new book into the database. + :returns: True if succeeds False if not """ with Session(self.engine) as session: @@ -61,15 +81,14 @@ class Storage: self.config.logger.error(f"{book[0][0:80]} :: {e}") def book_paths_list(self): - """ - Get file paths from database for comparison to system files - """ + """Get file paths from database for comparison to system files.""" session = Session(self.engine) _result = session.scalars(select(Book.file_name)).fetchall() session.close() return _result def make_collections(self): + """Make collections.""" # TODO: Check this still works with the switch to sqlalchemy self.config.logger.info("Making collections.") _title_regx = re.compile(r"^[0-9][0-9]*|-|\ \B") @@ -98,14 +117,17 @@ class Storage: ) _sess.close() if _q.fetchone() is None: - _collection = Collection(collection=_s, book_id=book.book_id) + _collection = Collection( + collection=_s, book_id=book.book_id) with Session(self.engine) as _sess: try: _sess.add(_collection) _sess.commit() _sess.close() - self.config.logger.info(f"Collection {_s} added.") + self.config.logger.info( + f"Collection {_s} added.") except Exception as e: - self.config.logger.error(f"Collection {_s} failed: {e}") + self.config.logger.error( + f"Collection {_s} failed: {e}") _collections.append(_p) self.config.logger.info("Finished making collections.") diff --git a/src/frontend/templates/index.html b/src/frontend/templates/index.html new file mode 100644 index 0000000..fbd956d --- /dev/null +++ b/src/frontend/templates/index.html @@ -0,0 +1,9 @@ + + + pyShelf Content Server + + + +

ID: {{ id }}

+ +