diff --git a/pyproject.toml b/pyproject.toml index f9a1a44..0065582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,12 @@ authors = [{ name = "Kristofers Solo", email = "dev@kristofers.xyz" }] readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } -dependencies = ["attrs==23.1.0", "customtkinter==5.2.1", "packaging==23.2"] +dependencies = [ + "attrs==23.1.0", + "customtkinter==5.2.1", + "loguru==0.7.2", + "packaging==23.2", +] [tool.mypy] check_untyped_defs = true diff --git a/requirements.txt b/requirements.txt index 83b9f65..5e35ec9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ attrs>=23.1.0 customtkinter>=5.2.1 +loguru>=0.7.2 packaging>=23.2 diff --git a/src/bookstore/book.py b/src/bookstore/book.py index a02e02d..5149860 100644 --- a/src/bookstore/book.py +++ b/src/bookstore/book.py @@ -1,18 +1,22 @@ from attrs import define, field, setters, validators +from loguru import logger from .isbn import ISBN +@logger.catch def _check_price_value(instance, attribute, value): if value < 0: raise ValueError("Price must be larger or equal to 0!") +@logger.catch def _check_stock_value(instance, attribute, value): if value < 0: raise ValueError("Stock must be larger or equal to 0!") +@logger.catch def _to_isbn(number: str): return ISBN(number) @@ -26,12 +30,15 @@ class Book: stock: int = field(converter=int, validator=[validators.instance_of(int), _check_stock_value]) @classmethod + @logger.catch def fields(cls) -> tuple[str, str, str, str, str]: return "ISBN", "Title", "Author", "Price", "Stock" + @logger.catch def values(self) -> tuple[ISBN, str, str, float, int]: return self.isbn, self.title, self.author, self.price, self.stock + @logger.catch def get(self, field: str, default: str = "") -> ISBN | str | float | int: match field: case "ISBN": @@ -47,5 +54,6 @@ class Book: case _: return default + @logger.catch def __iter__(self): yield from self.values() diff --git a/src/bookstore/inventory.py b/src/bookstore/inventory.py index 80d2e93..6f36af7 100644 --- a/src/bookstore/inventory.py +++ b/src/bookstore/inventory.py @@ -1,11 +1,14 @@ import sqlite3 from pathlib import Path +from loguru import logger + from .book import Book from .isbn import ISBN class Inventory: + @logger.catch def __init__(self, db_path: Path) -> None: self.conn = sqlite3.connect(db_path) self.cursor = self.conn.cursor() @@ -21,24 +24,28 @@ class Inventory: """ ) + @logger.catch def save(self) -> None: """Save `Inventory` to SQLite database.""" self.conn.commit() + @logger.catch def close(self) -> None: """Close database connection.""" self.conn.close() + @logger.catch def add(self, *books: Book) -> None: """Add `Book` to the `Inventory`. `Book`s ISBN must be unique.""" for book in books: try: self.cursor.execute("INSERT INTO Book VALUES (?, ?, ?, ?, ?)", (book.isbn, book.title, book.author, book.price, book.stock)) self.save() - print(f"Book with ISBN: {book.isbn} was successfully saved") + logger.info(f"Create: {book}") except sqlite3.InternalError as e: - print(f"A book with ISBN {book.isbn} already exists in the database.\n{e}") + logger.error(f"A book with ISBN {book.isbn} already exists in the database.\t{e}") + @logger.catch def edit(self, book: Book) -> None: """Edit `Book`.""" try: @@ -46,20 +53,22 @@ class Inventory: "UPDATE Book SET title = ?, author = ?, price = ?, stock = ? WHERE isbn = ?", (book.title, book.author, book.price, book.stock, book.isbn) ) self.save() - print(f"Book with ISBN: {book.isbn} was successfully updated!") + logger.info(f"Update: {book}") except sqlite3.InternalError as e: - print(f"Something went wrong.\n{e}") + logger.error(f"Something went wrong.\t{e}") + @logger.catch def delete(self, isbn: ISBN) -> Book | None: """Deletes `Book` from `Inventory` by `ISBN` and returns deleted `Book`""" deleted_book = self.find_by_isbn(isbn) self.cursor.execute("DELETE FROM Book WHERE isbn = ?", (isbn,)) self.save() - print(f"Book with ISBN: {isbn} was successfully deleted!") + logger.info(f"Delete: {deleted_book}") return deleted_book + @logger.catch def find_by_isbn(self, isbn: ISBN) -> Book | None: """Looks up `Book` within `Inventory` by book `ISBN` and returns it. Returns `None` if it doesn't exist.""" self.cursor.execute("SELECT * FROM Book WHERE isbn = ?", (isbn,)) @@ -68,6 +77,7 @@ class Inventory: return None return Book(*book) + @logger.catch def find_by_title(self, title: str) -> list[Book] | None: """Looks up `Book`s within `Inventory` by book title and returns them as a `List`. Returns `None` if none were found""" self.cursor.execute("SELECT * FROM Book WHERE title LIKE ?", (f"%{title}%",)) @@ -76,6 +86,7 @@ class Inventory: return None return [Book(*book) for book in books] + @logger.catch def find_by_author(self, author: str) -> list[Book] | None: """Looks up `Book`s within `Inventory` by book author and returns them as a `List`. Returns `None` if none were found""" self.cursor.execute("SELECT * FROM Book WHERE author LIKE ?", (f"%{author}%",)) @@ -84,6 +95,7 @@ class Inventory: return None return [Book(*book) for book in books] + @logger.catch def list_all(self) -> list[Book | None]: """Returns `List` of all `Book`s.""" self.cursor.execute("SELECT * FROM Book") diff --git a/src/bookstore/isbn.py b/src/bookstore/isbn.py index 082ec7d..4cf81ec 100644 --- a/src/bookstore/isbn.py +++ b/src/bookstore/isbn.py @@ -1,7 +1,6 @@ from attrs import define, field, validators -# TODO: create checksum method @define(frozen=True) class ISBN(str): number: str = field(converter=str, validator=validators.matches_re(r"^\d{10}$|^\d{13}$")) diff --git a/src/main.py b/src/main.py index a966ef8..b737034 100755 --- a/src/main.py +++ b/src/main.py @@ -4,9 +4,21 @@ from pathlib import Path from bookstore.inventory import Inventory + +from loguru import logger from ui.app import App +# Set up logging +logger.add( + Path("logs", "bookstore.log"), + format="{time} | {level} | {message}", + level="INFO", + rotation="1 MB", + compression="zip", +) + +@logger.catch def main() -> None: db_path = Path("db.sqlite3") inventory = Inventory(db_path)