feat(tui): display books as a table

This commit is contained in:
Kristofers Solo 2023-11-01 18:57:35 +02:00
parent 00adf20008
commit 8b8cdd00d0
11 changed files with 145 additions and 24 deletions

0
bookstore.sqlite Normal file
View File

View File

@ -1,9 +0,0 @@
#!/usr/bin/env python3
def main() -> None:
pass
if __name__ == "__main__":
main()

1
main.py Symbolic link
View File

@ -0,0 +1 @@
src/main.py

View File

@ -6,7 +6,7 @@ authors = [{ name = "Kristofers Solo", email = "dev@kristofers.xyz" }]
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
license = { text = "MIT" } license = { text = "MIT" }
dependencies = ["loguru==0.7", "attrs==23.1.0"] dependencies = ["attrs==23.1.0", "urwid==2.2.3"]
[tool.mypy] [tool.mypy]
check_untyped_defs = true check_untyped_defs = true

View File

@ -1,2 +1,2 @@
attrs==23.1.0 attrs>=23.1.0
loguru==0.7.0 urwid>=2.2.3

View File

@ -19,8 +19,15 @@ def _to_isbn(number: str):
@define @define
class Book: class Book:
title: str = field()
author: str = field() # TODO: add default author as "Unknown"
isbn: ISBN = field(converter=_to_isbn, validator=validators.instance_of(ISBN), on_setattr=setters.frozen, repr=lambda value: f"'{value}'") isbn: ISBN = field(converter=_to_isbn, validator=validators.instance_of(ISBN), on_setattr=setters.frozen, repr=lambda value: f"'{value}'")
title: str = field()
author: str = field()
price: float = field(converter=float, validator=[validators.instance_of(float), _check_price_value]) price: float = field(converter=float, validator=[validators.instance_of(float), _check_price_value])
stock: int = field(converter=int, validator=[validators.instance_of(int), _check_stock_value]) stock: int = field(converter=int, validator=[validators.instance_of(int), _check_stock_value])
@classmethod
def fields(cls) -> tuple[str, str, str, str, str]:
return "ISBN", "Title", "Author", "Price", "Stock"
def field_values(self) -> tuple[ISBN, str, str, float, int]:
return self.isbn, self.title, self.author, self.price, self.stock

View File

@ -29,12 +29,12 @@ class Inventory:
"""Close database connection.""" """Close database connection."""
self.conn.close() self.conn.close()
def add(self, *books: Book) -> None: def add(self, book: Book) -> None:
"""Add `Book` to the `Inventory`. `Book`s ISBN must be unique.""" """Add `Book` to the `Inventory`. `Book`s ISBN must be unique."""
for book in books:
try: try:
self.cursor.execute("INSERT INTO Book VALUES (?, ?, ?, ?, ?)", (book.isbn, book.title, book.author, book.price, book.stock)) self.cursor.execute("INSERT INTO Book VALUES (?, ?, ?, ?, ?)", (book.isbn, book.title, book.author, book.price, book.stock))
self.conn.commit() self.conn.commit()
print(f"Book with ISBN: {book.isbn} was saved")
except sqlite3.InternalError: except sqlite3.InternalError:
print(f"A book with ISBN {book.isbn} already exists in the database.") print(f"A book with ISBN {book.isbn} already exists in the database.")
@ -71,10 +71,8 @@ class Inventory:
return None return None
return [Book(*book) for book in books] return [Book(*book) for book in books]
def list_all(self) -> list[Book] | None: def list_all(self) -> list[Book | None]:
"""Returns `List` of all `Book`s.""" """Returns `List` of all `Book`s."""
self.cursor.execute("SELECT * FROM Book") self.cursor.execute("SELECT * FROM Book")
books = self.cursor.fetchall() books = self.cursor.fetchall()
if not books:
return None
return [Book(*book) for book in books] return [Book(*book) for book in books]

16
src/main.py Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
from pathlib import Path
from bookstore.inventory import Inventory
from ui import tui
def main() -> None:
db_path = Path("db.sqlite3")
inventory = Inventory(db_path)
tui.render(inventory)
if __name__ == "__main__":
main()

7
src/ui/handlers.py Normal file
View File

@ -0,0 +1,7 @@
import urwid
def exit_on_q(key):
"""Define a function to handle exit when the `q` key is pressed"""
if key in ("q", "Q"):
raise urwid.ExitMainLoop()

31
src/ui/tui.py Normal file
View File

@ -0,0 +1,31 @@
import urwid
from bookstore.book import Book
from bookstore.inventory import Inventory
from .handlers import exit_on_q
from .widgets.table import Table
FOCUS_STYLE = urwid.AttrMap("default", "dark red", "standout")
UNFOCUS_STYLE = urwid.AttrMap("default", "default", "standout")
def create_books_table(books: list[Book]) -> Table:
header = urwid.Columns([urwid.Text(header) for header in Book.fields()])
header = urwid.AttrMap(header, "header", focus_map=FOCUS_STYLE)
rows = [header]
for book in books:
row = urwid.Columns([urwid.Text(str(value)) for value in book.field_values()])
rows.append(urwid.AttrMap(row, "body", focus_map=FOCUS_STYLE))
walker = urwid.SimpleListWalker(rows)
books_table = Table(walker)
return books_table
def render(inventory: Inventory):
books_table = create_books_table(inventory.list_all())
loop = urwid.MainLoop(books_table, unhandled_input=exit_on_q)
loop.run()

64
src/ui/utils.py Normal file
View File

@ -0,0 +1,64 @@
import customtkinter as ctk
from bookstore.book import Book
from bookstore.inventory import Inventory
class UI:
def __init__(self, inventory: Inventory) -> None:
self.inventory = inventory
self.theme()
self.root = ctk.CTk()
self.root.geometry("650x400")
self.frame = ctk.CTkFrame(self.root)
def render(self) -> None:
self._create_gui()
self.root.mainloop()
def theme(self, mode: str = "dark", color: str = "dark-blue") -> None:
ctk.set_appearance_mode(mode)
ctk.set_default_color_theme(color)
def _create_gui(self) -> None:
self._show()
label = ctk.CTkLabel(self.frame, text="LU Bookstore")
label.pack(pady=12, padx=10)
button = ctk.CTkButton(self.frame, text="New book", command=self._add_book)
button.pack(pady=12, padx=10)
def _list_books(self) -> None:
pass
def _add_book(self) -> None:
self._hide()
self._show()
placeholders = ("ISBN", "Title", "Author", "Price", "Stock")
self.entries: list[ctk.CTkEntry] = []
for placeholder in placeholders:
entry = ctk.CTkEntry(self.frame, placeholder_text=placeholder)
entry.pack(pady=12, padx=10)
self.entries.append(entry)
button = ctk.CTkButton(self.frame, text="Save", command=self._save_book)
button.pack(pady=12, padx=10)
def _save_book(self) -> None:
entry_values = [entry.get() for entry in self.entries]
if entry_values[0]: # ISBN must be a value
new_book = Book(*entry_values)
self.inventory.add(new_book)
for entry in self.entries:
entry.destroy()
self.entries = []
self._show()
def _hide(self) -> None:
self.frame.pack_forget()
def _show(self) -> None:
self.frame.pack(pady=20, padx=60, fill="both", expand=True)

6
src/ui/widgets/table.py Normal file
View File

@ -0,0 +1,6 @@
import urwid
class Table(urwid.ListBox):
def __init__(self, body):
super().__init__(body)