mirror of
https://github.com/kristoferssolo/LU-bookstore.git
synced 2025-10-21 18:00:34 +00:00
feat(tui): display books as a table
This commit is contained in:
parent
00adf20008
commit
8b8cdd00d0
0
bookstore.sqlite
Normal file
0
bookstore.sqlite
Normal file
9
main.py
9
main.py
@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
attrs==23.1.0
|
attrs>=23.1.0
|
||||||
loguru==0.7.0
|
urwid>=2.2.3
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -29,14 +29,14 @@ 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.")
|
||||||
|
|
||||||
def delete(self, isbn: ISBN) -> Book | None:
|
def delete(self, isbn: ISBN) -> Book | None:
|
||||||
"""Deletes `Book` from `Inventory` by `ISBN` and returns deleted `Book`"""
|
"""Deletes `Book` from `Inventory` by `ISBN` and returns deleted `Book`"""
|
||||||
@ -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
16
src/main.py
Executable 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
7
src/ui/handlers.py
Normal 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
31
src/ui/tui.py
Normal 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
64
src/ui/utils.py
Normal 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
6
src/ui/widgets/table.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import urwid
|
||||||
|
|
||||||
|
|
||||||
|
class Table(urwid.ListBox):
|
||||||
|
def __init__(self, body):
|
||||||
|
super().__init__(body)
|
||||||
Loading…
Reference in New Issue
Block a user