treewide: format and lint

This commit is contained in:
Oystein Kristoffer Tveit 2025-03-22 21:15:15 +01:00
parent a7ff594548
commit 46f6de5d61
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
55 changed files with 1269 additions and 987 deletions

@ -1 +1,3 @@
from .main import main from .main import main
__all__ = ["main"]

@ -1 +1,3 @@
from .main import WorblehatCli from .main import WorblehatCli
__all__ = ["WorblehatCli"]

@ -30,19 +30,20 @@ from .subclis import (
# However, is there anyone who are going to search by category rather than just look in # However, is there anyone who are going to search by category rather than just look in
# the shelves? # the shelves?
class WorblehatCli(NumberedCmd): class WorblehatCli(NumberedCmd):
def __init__(self, sql_session: Session): def __init__(self, sql_session: Session):
super().__init__() super().__init__()
self.sql_session = sql_session self.sql_session = sql_session
self.sql_session_dirty = False self.sql_session_dirty = False
@event.listens_for(self.sql_session, 'after_flush') @event.listens_for(self.sql_session, "after_flush")
def mark_session_as_dirty(*_): def mark_session_as_dirty(*_):
self.sql_session_dirty = True self.sql_session_dirty = True
self.prompt_header = f'(unsaved changes)' self.prompt_header = "(unsaved changes)"
@event.listens_for(self.sql_session, 'after_commit') @event.listens_for(self.sql_session, "after_commit")
@event.listens_for(self.sql_session, 'after_rollback') @event.listens_for(self.sql_session, "after_rollback")
def mark_session_as_clean(*_): def mark_session_as_clean(*_):
self.sql_session_dirty = False self.sql_session_dirty = False
self.prompt_header = None self.prompt_header = None
@ -57,19 +58,20 @@ class WorblehatCli(NumberedCmd):
if not tool.sql_session_dirty: if not tool.sql_session_dirty:
exit(0) exit(0)
try: try:
print() print()
if prompt_yes_no('Are you sure you want to exit without saving?', default=False): if prompt_yes_no(
raise KeyboardInterrupt "Are you sure you want to exit without saving?", default=False
):
raise KeyboardInterrupt
except KeyboardInterrupt: except KeyboardInterrupt:
if tool.sql_session is not None: if tool.sql_session is not None:
tool.sql_session.rollback() tool.sql_session.rollback()
exit(0) exit(0)
def do_show_bookcase(self, arg: str): def do_show_bookcase(self, arg: str):
bookcase_selector = InteractiveItemSelector( bookcase_selector = InteractiveItemSelector(
cls = Bookcase, cls=Bookcase,
sql_session = self.sql_session, sql_session=self.sql_session,
) )
bookcase_selector.cmdloop() bookcase_selector.cmdloop()
bookcase = bookcase_selector.result bookcase = bookcase_selector.result
@ -77,8 +79,7 @@ class WorblehatCli(NumberedCmd):
for shelf in bookcase.shelfs: for shelf in bookcase.shelfs:
print(shelf.short_str()) print(shelf.short_str())
for item in shelf.items: for item in shelf.items:
print(f' {item.name} - {item.amount} copies') print(f" {item.name} - {item.amount} copies")
def do_show_borrowed_queued(self, _: str): def do_show_borrowed_queued(self, _: str):
borrowed_items = self.sql_session.scalars( borrowed_items = self.sql_session.scalars(
@ -88,105 +89,114 @@ class WorblehatCli(NumberedCmd):
).all() ).all()
if len(borrowed_items) == 0: if len(borrowed_items) == 0:
print('No borrowed items found.') print("No borrowed items found.")
else: else:
print('Borrowed items:') print("Borrowed items:")
for item in borrowed_items: for item in borrowed_items:
print(f'- {item.username} - {item.item.name} - to be delivered by {item.end_time.strftime("%Y-%m-%d")}') print(
f"- {item.username} - {item.item.name} - to be delivered by {item.end_time.strftime('%Y-%m-%d')}"
)
print() print()
queued_items = self.sql_session.scalars( queued_items = self.sql_session.scalars(
select(BookcaseItemBorrowingQueue) select(BookcaseItemBorrowingQueue).order_by(
.order_by(BookcaseItemBorrowingQueue.entered_queue_time), BookcaseItemBorrowingQueue.entered_queue_time
),
).all() ).all()
if len(queued_items) == 0: if len(queued_items) == 0:
print('No queued items found.') print("No queued items found.")
else: else:
print('Users in queue:') print("Users in queue:")
for item in queued_items: for item in queued_items:
print(f'- {item.username} - {item.item.name} - entered queue at {item.entered_queue_time.strftime("%Y-%m-%d")}') print(
f"- {item.username} - {item.item.name} - entered queue at {item.entered_queue_time.strftime('%Y-%m-%d')}"
)
def _create_bookcase_item(self, isbn: str): def _create_bookcase_item(self, isbn: str):
bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session) bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session)
if bookcase_item is None: if bookcase_item is None:
print(f'Could not find data about item with ISBN {isbn} online.') print(f"Could not find data about item with ISBN {isbn} online.")
print(f'If you think this is not due to a bug, please add the book to openlibrary.org before continuing.') print(
"If you think this is not due to a bug, please add the book to openlibrary.org before continuing."
)
return return
else: else:
print(dedent(f""" print(
dedent(f"""
Found item: Found item:
title: {bookcase_item.name} title: {bookcase_item.name}
authors: {', '.join(a.name for a in bookcase_item.authors)} authors: {", ".join(a.name for a in bookcase_item.authors)}
language: {bookcase_item.language} language: {bookcase_item.language}
""")) """)
)
print('Please select the bookcase where the item is placed:') print("Please select the bookcase where the item is placed:")
bookcase_selector = InteractiveItemSelector( bookcase_selector = InteractiveItemSelector(
cls = Bookcase, cls=Bookcase,
sql_session = self.sql_session, sql_session=self.sql_session,
) )
bookcase_selector.cmdloop() bookcase_selector.cmdloop()
bookcase = bookcase_selector.result bookcase = bookcase_selector.result
bookcase_item.shelf = select_bookcase_shelf(bookcase, self.sql_session) bookcase_item.shelf = select_bookcase_shelf(bookcase, self.sql_session)
print('Please select the items media type:') print("Please select the items media type:")
media_type_selector = InteractiveItemSelector( media_type_selector = InteractiveItemSelector(
cls = MediaType, cls=MediaType,
sql_session = self.sql_session, sql_session=self.sql_session,
default = self.sql_session.scalars( default=self.sql_session.scalars(
select(MediaType) select(MediaType).where(MediaType.name.ilike("book")),
.where(MediaType.name.ilike("book")),
).one(), ).one(),
) )
media_type_selector.cmdloop() media_type_selector.cmdloop()
bookcase_item.media_type = media_type_selector.result bookcase_item.media_type = media_type_selector.result
username = input('Who owns this book? [PVV]> ') username = input("Who owns this book? [PVV]> ")
if username != '': if username != "":
bookcase_item.owner = username bookcase_item.owner = username
self.sql_session.add(bookcase_item) self.sql_session.add(bookcase_item)
self.sql_session.flush() self.sql_session.flush()
def default(self, isbn: str): def default(self, isbn: str):
isbn = isbn.strip() isbn = isbn.strip()
if not is_valid_isbn(isbn): if not is_valid_isbn(isbn):
super()._default(isbn) super()._default(isbn)
return return
if (existing_item := self.sql_session.scalars( if (
select(BookcaseItem) existing_item := self.sql_session.scalars(
.where(BookcaseItem.isbn == isbn) select(BookcaseItem)
.join(BookcaseItemBorrowing) .where(BookcaseItem.isbn == isbn)
.join(BookcaseItemBorrowingQueue) .join(BookcaseItemBorrowing)
).one_or_none()) is not None: .join(BookcaseItemBorrowingQueue)
).one_or_none()
) is not None:
print(f'\nFound existing item for isbn "{isbn}"') print(f'\nFound existing item for isbn "{isbn}"')
BookcaseItemCli( BookcaseItemCli(
sql_session = self.sql_session, sql_session=self.sql_session,
bookcase_item = existing_item, bookcase_item=existing_item,
).cmdloop() ).cmdloop()
return return
if prompt_yes_no(f"Could not find item with ISBN '{isbn}'.\nWould you like to create it?", default=True): if prompt_yes_no(
f"Could not find item with ISBN '{isbn}'.\nWould you like to create it?",
default=True,
):
self._create_bookcase_item(isbn) self._create_bookcase_item(isbn)
def do_search(self, _: str): def do_search(self, _: str):
search_cli = SearchCli(self.sql_session) search_cli = SearchCli(self.sql_session)
search_cli.cmdloop() search_cli.cmdloop()
if search_cli.result is not None: if search_cli.result is not None:
BookcaseItemCli( BookcaseItemCli(
sql_session = self.sql_session, sql_session=self.sql_session,
bookcase_item = search_cli.result, bookcase_item=search_cli.result,
).cmdloop() ).cmdloop()
def do_show_slabbedasker(self, _: str): def do_show_slabbedasker(self, _: str):
slubberter = self.sql_session.scalars( slubberter = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing)
@ -201,76 +211,73 @@ class WorblehatCli(NumberedCmd):
).all() ).all()
if len(slubberter) == 0: if len(slubberter) == 0:
print('No slubberts found. Life is good.') print("No slubberts found. Life is good.")
return return
for slubbert in slubberter: for slubbert in slubberter:
print('Slubberter:') print("Slubberter:")
print(f'- {slubbert.username} - {slubbert.item.name} - {slubbert.end_time.strftime("%Y-%m-%d")}') print(
f"- {slubbert.username} - {slubbert.item.name} - {slubbert.end_time.strftime('%Y-%m-%d')}"
)
def do_advanced(self, _: str): def do_advanced(self, _: str):
AdvancedOptionsCli(self.sql_session).cmdloop() AdvancedOptionsCli(self.sql_session).cmdloop()
def do_save(self, _: str):
def do_save(self, _:str):
if not self.sql_session_dirty: if not self.sql_session_dirty:
print('No changes to save.') print("No changes to save.")
return return
self.sql_session.commit() self.sql_session.commit()
def do_abort(self, _: str):
def do_abort(self, _:str):
if not self.sql_session_dirty: if not self.sql_session_dirty:
print('No changes to abort.') print("No changes to abort.")
return return
self.sql_session.rollback() self.sql_session.rollback()
def do_exit(self, _: str): def do_exit(self, _: str):
if self.sql_session_dirty: if self.sql_session_dirty:
if prompt_yes_no('Would you like to save your changes?'): if prompt_yes_no("Would you like to save your changes?"):
self.sql_session.commit() self.sql_session.commit()
else: else:
self.sql_session.rollback() self.sql_session.rollback()
exit(0) exit(0)
funcs = { funcs = {
0: { 0: {
'f': default, "f": default,
'doc': 'Choose / Add item with its ISBN', "doc": "Choose / Add item with its ISBN",
}, },
1: { 1: {
'f': do_search, "f": do_search,
'doc': 'Search', "doc": "Search",
}, },
2: { 2: {
'f': do_show_bookcase, "f": do_show_bookcase,
'doc': 'Show a bookcase, and its items', "doc": "Show a bookcase, and its items",
}, },
3: { 3: {
'f': do_show_borrowed_queued, "f": do_show_borrowed_queued,
'doc': 'Show borrowed/queued items', "doc": "Show borrowed/queued items",
}, },
4: { 4: {
'f': do_show_slabbedasker, "f": do_show_slabbedasker,
'doc': 'Show slabbedasker', "doc": "Show slabbedasker",
}, },
5: { 5: {
'f': do_save, "f": do_save,
'doc': 'Save changes', "doc": "Save changes",
}, },
6: { 6: {
'f': do_abort, "f": do_abort,
'doc': 'Abort changes', "doc": "Abort changes",
}, },
7: { 7: {
'f': do_advanced, "f": do_advanced,
'doc': 'Advanced options', "doc": "Advanced options",
}, },
9: { 9: {
'f': do_exit, "f": do_exit,
'doc': 'Exit', "doc": "Exit",
}, },
} }

@ -1,4 +1,11 @@
from .advanced_options import AdvancedOptionsCli from .advanced_options import AdvancedOptionsCli
from .bookcase_item import BookcaseItemCli from .bookcase_item import BookcaseItemCli
from .bookcase_shelf_selector import select_bookcase_shelf from .bookcase_shelf_selector import select_bookcase_shelf
from .search import SearchCli from .search import SearchCli
__all__ = [
"AdvancedOptionsCli",
"BookcaseItemCli",
"select_bookcase_shelf",
"SearchCli",
]

@ -4,81 +4,84 @@ from sqlalchemy.orm import Session
from libdib.repl import ( from libdib.repl import (
InteractiveItemSelector, InteractiveItemSelector,
NumberedCmd, NumberedCmd,
format_date,
prompt_yes_no,
) )
from worblehat.models import Bookcase, BookcaseShelf from worblehat.models import Bookcase, BookcaseShelf
class AdvancedOptionsCli(NumberedCmd): class AdvancedOptionsCli(NumberedCmd):
def __init__(self, sql_session: Session): def __init__(self, sql_session: Session):
super().__init__() super().__init__()
self.sql_session = sql_session self.sql_session = sql_session
def do_add_bookcase(self, _: str): def do_add_bookcase(self, _: str):
while True: while True:
name = input('Name of bookcase> ') name = input("Name of bookcase> ")
if name == '': if name == "":
print('Error: name cannot be empty') print("Error: name cannot be empty")
continue continue
if self.sql_session.scalars( if (
select(Bookcase) self.sql_session.scalars(
.where(Bookcase.name == name) select(Bookcase).where(Bookcase.name == name)
).one_or_none() is not None: ).one_or_none()
print(f'Error: a bookcase with name {name} already exists') is not None
):
print(f"Error: a bookcase with name {name} already exists")
continue continue
break break
description = input('Description of bookcase> ') description = input("Description of bookcase> ")
if description == '': if description == "":
description = None description = None
bookcase = Bookcase(name, description) bookcase = Bookcase(name, description)
self.sql_session.add(bookcase) self.sql_session.add(bookcase)
self.sql_session.flush() self.sql_session.flush()
def do_add_bookcase_shelf(self, arg: str): def do_add_bookcase_shelf(self, arg: str):
bookcase_selector = InteractiveItemSelector( bookcase_selector = InteractiveItemSelector(
cls = Bookcase, cls=Bookcase,
sql_session = self.sql_session, sql_session=self.sql_session,
) )
bookcase_selector.cmdloop() bookcase_selector.cmdloop()
bookcase = bookcase_selector.result bookcase = bookcase_selector.result
while True: while True:
column = input('Column> ') column = input("Column> ")
try: try:
column = int(column) column = int(column)
except ValueError: except ValueError:
print('Error: column must be a number') print("Error: column must be a number")
continue continue
break break
while True: while True:
row = input('Row> ') row = input("Row> ")
try: try:
row = int(row) row = int(row)
except ValueError: except ValueError:
print('Error: row must be a number') print("Error: row must be a number")
continue continue
break break
if self.sql_session.scalars( if (
select(BookcaseShelf) self.sql_session.scalars(
.where( select(BookcaseShelf).where(
BookcaseShelf.bookcase == bookcase, BookcaseShelf.bookcase == bookcase,
BookcaseShelf.column == column, BookcaseShelf.column == column,
BookcaseShelf.row == row, BookcaseShelf.row == row,
)
).one_or_none()
is not None
):
print(
f"Error: a bookshelf in bookcase {bookcase.name} with position c{column}-r{row} already exists"
) )
).one_or_none() is not None:
print(f'Error: a bookshelf in bookcase {bookcase.name} with position c{column}-r{row} already exists')
return return
description = input('Description> ') description = input("Description> ")
if description == '': if description == "":
description = None description = None
shelf = BookcaseShelf( shelf = BookcaseShelf(
@ -90,15 +93,14 @@ class AdvancedOptionsCli(NumberedCmd):
self.sql_session.add(shelf) self.sql_session.add(shelf)
self.sql_session.flush() self.sql_session.flush()
def do_list_bookcases(self, _: str): def do_list_bookcases(self, _: str):
bookcase_shelfs = self.sql_session.scalars( bookcase_shelfs = self.sql_session.scalars(
select(BookcaseShelf) select(BookcaseShelf)
.join(Bookcase) .join(Bookcase)
.order_by( .order_by(
Bookcase.name, Bookcase.name,
BookcaseShelf.column, BookcaseShelf.column,
BookcaseShelf.row, BookcaseShelf.row,
) )
).all() ).all()
@ -108,28 +110,26 @@ class AdvancedOptionsCli(NumberedCmd):
print(shelf.bookcase.short_str()) print(shelf.bookcase.short_str())
bookcase_uid = shelf.bookcase.uid bookcase_uid = shelf.bookcase.uid
print(f' {shelf.short_str()} - {sum(i.amount for i in shelf.items)} items') print(f" {shelf.short_str()} - {sum(i.amount for i in shelf.items)} items")
def do_done(self, _: str): def do_done(self, _: str):
return True return True
funcs = { funcs = {
1: { 1: {
'f': do_add_bookcase, "f": do_add_bookcase,
'doc': 'Add bookcase', "doc": "Add bookcase",
}, },
2: { 2: {
'f': do_add_bookcase_shelf, "f": do_add_bookcase_shelf,
'doc': 'Add bookcase shelf', "doc": "Add bookcase shelf",
}, },
3: { 3: {
'f': do_list_bookcases, "f": do_list_bookcases,
'doc': 'List all bookcases', "doc": "List all bookcases",
}, },
9: { 9: {
'f': do_done, "f": do_done,
'doc': 'Done', "doc": "Done",
}, },
} }

@ -27,16 +27,18 @@ from worblehat.services.config import Config
from .bookcase_shelf_selector import select_bookcase_shelf from .bookcase_shelf_selector import select_bookcase_shelf
def _selected_bookcase_item_prompt(bookcase_item: BookcaseItem) -> str: def _selected_bookcase_item_prompt(bookcase_item: BookcaseItem) -> str:
amount_borrowed = len(bookcase_item.borrowings) amount_borrowed = len(bookcase_item.borrowings)
return dedent(f''' return dedent(f"""
Item: {bookcase_item.name} Item: {bookcase_item.name}
ISBN: {bookcase_item.isbn} ISBN: {bookcase_item.isbn}
Authors: {', '.join(a.name for a in bookcase_item.authors)} Authors: {", ".join(a.name for a in bookcase_item.authors)}
Bookcase: {bookcase_item.shelf.bookcase.short_str()} Bookcase: {bookcase_item.shelf.bookcase.short_str()}
Shelf: {bookcase_item.shelf.short_str()} Shelf: {bookcase_item.shelf.short_str()}
Amount: {bookcase_item.amount - amount_borrowed}/{bookcase_item.amount} Amount: {bookcase_item.amount - amount_borrowed}/{bookcase_item.amount}
''') """)
class BookcaseItemCli(NumberedCmd): class BookcaseItemCli(NumberedCmd):
def __init__(self, sql_session: Session, bookcase_item: BookcaseItem): def __init__(self, sql_session: Session, bookcase_item: BookcaseItem):
@ -44,12 +46,10 @@ class BookcaseItemCli(NumberedCmd):
self.sql_session = sql_session self.sql_session = sql_session
self.bookcase_item = bookcase_item self.bookcase_item = bookcase_item
@property @property
def prompt_header(self) -> str: def prompt_header(self) -> str:
return _selected_bookcase_item_prompt(self.bookcase_item) return _selected_bookcase_item_prompt(self.bookcase_item)
def do_update_data(self, _: str): def do_update_data(self, _: str):
item = create_bookcase_item_from_isbn(self.sql_session, self.bookcase_item.isbn) item = create_bookcase_item_from_isbn(self.sql_session, self.bookcase_item.isbn)
self.bookcase_item.name = item.name self.bookcase_item.name = item.name
@ -58,80 +58,83 @@ class BookcaseItemCli(NumberedCmd):
self.bookcase_item.language = item.language self.bookcase_item.language = item.language
self.sql_session.flush() self.sql_session.flush()
def do_edit(self, arg: str): def do_edit(self, arg: str):
EditBookcaseCli(self.sql_session, self.bookcase_item, self).cmdloop() EditBookcaseCli(self.sql_session, self.bookcase_item, self).cmdloop()
@staticmethod @staticmethod
def _prompt_username() -> str: def _prompt_username() -> str:
while True: while True:
username = input('Username: ') username = input("Username: ")
if prompt_yes_no(f'Is {username} correct?', default = True): if prompt_yes_no(f"Is {username} correct?", default=True):
return username return username
def _has_active_borrowing(self, username: str) -> bool: def _has_active_borrowing(self, username: str) -> bool:
return self.sql_session.scalars( return (
select(BookcaseItemBorrowing) self.sql_session.scalars(
.where( select(BookcaseItemBorrowing).where(
BookcaseItemBorrowing.username == username, BookcaseItemBorrowing.username == username,
BookcaseItemBorrowing.item == self.bookcase_item, BookcaseItemBorrowing.item == self.bookcase_item,
BookcaseItemBorrowing.delivered.is_(None), BookcaseItemBorrowing.delivered.is_(None),
) )
).one_or_none() is not None ).one_or_none()
is not None
)
def _has_borrowing_queue_item(self, username: str) -> bool: def _has_borrowing_queue_item(self, username: str) -> bool:
return self.sql_session.scalars( return (
select(BookcaseItemBorrowingQueue) self.sql_session.scalars(
.where( select(BookcaseItemBorrowingQueue).where(
BookcaseItemBorrowingQueue.username == username, BookcaseItemBorrowingQueue.username == username,
BookcaseItemBorrowingQueue.item == self.bookcase_item, BookcaseItemBorrowingQueue.item == self.bookcase_item,
) )
).one_or_none() is not None ).one_or_none()
is not None
)
def do_borrow(self, _: str): def do_borrow(self, _: str):
active_borrowings = self.sql_session.scalars( active_borrowings = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing)
.where( .where(
BookcaseItemBorrowing.item == self.bookcase_item, BookcaseItemBorrowing.item == self.bookcase_item,
BookcaseItemBorrowing.delivered.is_(None), BookcaseItemBorrowing.delivered.is_(None),
) )
.order_by(BookcaseItemBorrowing.end_time) .order_by(BookcaseItemBorrowing.end_time)
).all() ).all()
if len(active_borrowings) >= self.bookcase_item.amount: if len(active_borrowings) >= self.bookcase_item.amount:
print('This item is currently not available') print("This item is currently not available")
print() print()
print('Active borrowings:') print("Active borrowings:")
for b in active_borrowings: for b in active_borrowings:
print(f' {b.username} - Until {format_date(b.end_time)}') print(f" {b.username} - Until {format_date(b.end_time)}")
if len(self.bookcase_item.borrowing_queue) > 0: if len(self.bookcase_item.borrowing_queue) > 0:
print('Borrowing queue:') print("Borrowing queue:")
for i, b in enumerate(self.bookcase_item.borrowing_queue): for i, b in enumerate(self.bookcase_item.borrowing_queue):
print(f' {i + 1} - {b.username}') print(f" {i + 1} - {b.username}")
print() print()
if not prompt_yes_no('Would you like to enter the borrowing queue?', default = True): if not prompt_yes_no(
"Would you like to enter the borrowing queue?", default=True
):
return return
username = self._prompt_username() username = self._prompt_username()
if self._has_active_borrowing(username): if self._has_active_borrowing(username):
print('You already have an active borrowing') print("You already have an active borrowing")
return return
if self._has_borrowing_queue_item(username): if self._has_borrowing_queue_item(username):
print('You are already in the borrowing queue') print("You are already in the borrowing queue")
return return
borrowing_queue_item = BookcaseItemBorrowingQueue(username, self.bookcase_item) borrowing_queue_item = BookcaseItemBorrowingQueue(
username, self.bookcase_item
)
self.sql_session.add(borrowing_queue_item) self.sql_session.add(borrowing_queue_item)
print(f'{username} entered the queue!') print(f"{username} entered the queue!")
return return
username = self._prompt_username() username = self._prompt_username()
@ -139,33 +142,38 @@ class BookcaseItemCli(NumberedCmd):
borrowing_item = BookcaseItemBorrowing(username, self.bookcase_item) borrowing_item = BookcaseItemBorrowing(username, self.bookcase_item)
self.sql_session.add(borrowing_item) self.sql_session.add(borrowing_item)
self.sql_session.flush() self.sql_session.flush()
print(f'Successfully borrowed the item. Please deliver it back by {format_date(borrowing_item.end_time)}') print(
f"Successfully borrowed the item. Please deliver it back by {format_date(borrowing_item.end_time)}"
)
def do_deliver(self, _: str): def do_deliver(self, _: str):
borrowings = self.sql_session.scalars( borrowings = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing)
.join(BookcaseItem, BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid) .join(
BookcaseItem,
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
)
.where(BookcaseItem.isbn == self.bookcase_item.isbn) .where(BookcaseItem.isbn == self.bookcase_item.isbn)
.order_by(BookcaseItemBorrowing.username) .order_by(BookcaseItemBorrowing.username)
).all() ).all()
if len(borrowings) == 0: if len(borrowings) == 0:
print('No one seems to have borrowed this item') print("No one seems to have borrowed this item")
return return
print('Borrowers:') print("Borrowers:")
for i, b in enumerate(borrowings): for i, b in enumerate(borrowings):
print(f' {i + 1}) {b.username}') print(f" {i + 1}) {b.username}")
while True: while True:
try: try:
selection = int(input('> ')) selection = int(input("> "))
except ValueError: except ValueError:
print('Error: selection must be an integer') print("Error: selection must be an integer")
continue continue
if selection < 1 or selection > len(borrowings): if selection < 1 or selection > len(borrowings):
print('Error: selection out of range') print("Error: selection out of range")
continue continue
break break
@ -173,19 +181,21 @@ class BookcaseItemCli(NumberedCmd):
borrowing = borrowings[selection - 1] borrowing = borrowings[selection - 1]
borrowing.delivered = datetime.now() borrowing.delivered = datetime.now()
self.sql_session.flush() self.sql_session.flush()
print(f'Successfully delivered the item for {borrowing.username}') print(f"Successfully delivered the item for {borrowing.username}")
def do_extend_borrowing(self, _: str): def do_extend_borrowing(self, _: str):
borrowings = self.sql_session.scalars( borrowings = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing)
.join(BookcaseItem, BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid) .join(
BookcaseItem,
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
)
.where(BookcaseItem.isbn == self.bookcase_item.isbn) .where(BookcaseItem.isbn == self.bookcase_item.isbn)
.order_by(BookcaseItemBorrowing.username) .order_by(BookcaseItemBorrowing.username)
).all() ).all()
if len(borrowings) == 0: if len(borrowings) == 0:
print('No one seems to have borrowed this item') print("No one seems to have borrowed this item")
return return
borrowing_queue = self.sql_session.scalars( borrowing_queue = self.sql_session.scalars(
@ -198,61 +208,68 @@ class BookcaseItemCli(NumberedCmd):
).all() ).all()
if len(borrowing_queue) != 0: if len(borrowing_queue) != 0:
print('Sorry, you cannot extend the borrowing because there are people waiting in the queue') print(
print('Borrowing queue:') "Sorry, you cannot extend the borrowing because there are people waiting in the queue"
for i, b in enumerate(borrowing_queue): )
print(f' {i + 1}) {b.username}') print("Borrowing queue:")
return for i, b in enumerate(borrowing_queue):
print(f" {i + 1}) {b.username}")
return
print('Who are you?') print("Who are you?")
selector = NumberedItemSelector( selector = NumberedItemSelector(
items = list(borrowings), items=list(borrowings),
stringify = lambda b: f'{b.username} - Until {format_date(b.end_time)}', stringify=lambda b: f"{b.username} - Until {format_date(b.end_time)}",
) )
selector.cmdloop() selector.cmdloop()
if selector.result is None: if selector.result is None:
return return
borrowing = selector.result borrowing = selector.result
borrowing.end_time = datetime.now() + timedelta(days=int(Config['deadline_daemon.days_before_queue_position_expires'])) borrowing.end_time = datetime.now() + timedelta(
days=int(Config["deadline_daemon.days_before_queue_position_expires"])
)
self.sql_session.flush() self.sql_session.flush()
print(f'Successfully extended the borrowing for {borrowing.username} until {format_date(borrowing.end_time)}') print(
f"Successfully extended the borrowing for {borrowing.username} until {format_date(borrowing.end_time)}"
)
def do_done(self, _: str): def do_done(self, _: str):
return True return True
funcs = { funcs = {
1: { 1: {
'f': do_borrow, "f": do_borrow,
'doc': 'Borrow', "doc": "Borrow",
}, },
2: { 2: {
'f': do_deliver, "f": do_deliver,
'doc': 'Deliver', "doc": "Deliver",
}, },
3: { 3: {
'f': do_extend_borrowing, "f": do_extend_borrowing,
'doc': 'Extend borrowing', "doc": "Extend borrowing",
}, },
4: { 4: {
'f': do_edit, "f": do_edit,
'doc': 'Edit', "doc": "Edit",
}, },
5: { 5: {
'f': do_update_data, "f": do_update_data,
'doc': 'Pull updated data from online databases', "doc": "Pull updated data from online databases",
}, },
9: { 9: {
'f': do_done, "f": do_done,
'doc': 'Done', "doc": "Done",
}, },
} }
class EditBookcaseCli(NumberedCmd): class EditBookcaseCli(NumberedCmd):
def __init__(self, sql_session: Session, bookcase_item: BookcaseItem, parent: BookcaseItemCli): def __init__(
self, sql_session: Session, bookcase_item: BookcaseItem, parent: BookcaseItemCli
):
super().__init__() super().__init__()
self.sql_session = sql_session self.sql_session = sql_session
self.bookcase_item = bookcase_item self.bookcase_item = bookcase_item
@ -260,54 +277,56 @@ class EditBookcaseCli(NumberedCmd):
@property @property
def prompt_header(self) -> str: def prompt_header(self) -> str:
return _selected_bookcase_item_prompt(self.bookcase_item) return _selected_bookcase_item_prompt(self.bookcase_item)
def do_name(self, _: str): def do_name(self, _: str):
while True: while True:
name = input('New name> ') name = input("New name> ")
if name == '': if name == "":
print('Error: name cannot be empty') print("Error: name cannot be empty")
continue continue
if self.sql_session.scalars( if (
select(BookcaseItem) self.sql_session.scalars(
.where(BookcaseItem.name == name) select(BookcaseItem).where(BookcaseItem.name == name)
).one_or_none() is not None: ).one_or_none()
print(f'Error: an item with name {name} already exists') is not None
):
print(f"Error: an item with name {name} already exists")
continue continue
break break
self.bookcase_item.name = name self.bookcase_item.name = name
self.sql_session.flush() self.sql_session.flush()
def do_isbn(self, _: str): def do_isbn(self, _: str):
while True: while True:
isbn = input('New ISBN> ') isbn = input("New ISBN> ")
if isbn == '': if isbn == "":
print('Error: ISBN cannot be empty') print("Error: ISBN cannot be empty")
continue continue
if not is_valid_isbn(isbn): if not is_valid_isbn(isbn):
print('Error: ISBN is not valid') print("Error: ISBN is not valid")
continue continue
if self.sql_session.scalars( if (
select(BookcaseItem) self.sql_session.scalars(
.where(BookcaseItem.isbn == isbn) select(BookcaseItem).where(BookcaseItem.isbn == isbn)
).one_or_none() is not None: ).one_or_none()
print(f'Error: an item with ISBN {isbn} already exists') is not None
):
print(f"Error: an item with ISBN {isbn} already exists")
continue continue
break break
self.bookcase_item.isbn = isbn self.bookcase_item.isbn = isbn
if prompt_yes_no('Update data from online databases?'): if prompt_yes_no("Update data from online databases?"):
self.parent.do_update_data('') self.parent.do_update_data("")
self.sql_session.flush() self.sql_session.flush()
def do_language(self, _: str): def do_language(self, _: str):
language_selector = InteractiveItemSelector( language_selector = InteractiveItemSelector(
Language, Language,
@ -317,7 +336,6 @@ class EditBookcaseCli(NumberedCmd):
self.bookcase_item.language = language_selector.result self.bookcase_item.language = language_selector.result
self.sql_session.flush() self.sql_session.flush()
def do_media_type(self, _: str): def do_media_type(self, _: str):
media_type_selector = InteractiveItemSelector( media_type_selector = InteractiveItemSelector(
MediaType, MediaType,
@ -327,24 +345,24 @@ class EditBookcaseCli(NumberedCmd):
self.bookcase_item.media_type = media_type_selector.result self.bookcase_item.media_type = media_type_selector.result
self.sql_session.flush() self.sql_session.flush()
def do_amount(self, _: str): def do_amount(self, _: str):
while (new_amount := input(f'New amount [{self.bookcase_item.amount}]> ')) != '': while (
new_amount := input(f"New amount [{self.bookcase_item.amount}]> ")
) != "":
try: try:
new_amount = int(new_amount) new_amount = int(new_amount)
except ValueError: except ValueError:
print('Error: amount must be an integer') print("Error: amount must be an integer")
continue continue
if new_amount < 1: if new_amount < 1:
print('Error: amount must be greater than 0') print("Error: amount must be greater than 0")
continue continue
break break
self.bookcase_item.amount = new_amount self.bookcase_item.amount = new_amount
self.sql_session.flush() self.sql_session.flush()
def do_shelf(self, _: str): def do_shelf(self, _: str):
bookcase_selector = InteractiveItemSelector( bookcase_selector = InteractiveItemSelector(
Bookcase, Bookcase,
@ -358,38 +376,36 @@ class EditBookcaseCli(NumberedCmd):
self.bookcase_item.shelf = shelf self.bookcase_item.shelf = shelf
self.sql_session.flush() self.sql_session.flush()
def do_done(self, _: str): def do_done(self, _: str):
return True return True
funcs = { funcs = {
1: { 1: {
'f': do_name, "f": do_name,
'doc': 'Change name', "doc": "Change name",
}, },
2: { 2: {
'f': do_isbn, "f": do_isbn,
'doc': 'Change ISBN', "doc": "Change ISBN",
}, },
3: { 3: {
'f': do_language, "f": do_language,
'doc': 'Change language', "doc": "Change language",
}, },
4: { 4: {
'f': do_media_type, "f": do_media_type,
'doc': 'Change media type', "doc": "Change media type",
}, },
5: { 5: {
'f': do_amount, "f": do_amount,
'doc': 'Change amount', "doc": "Change amount",
}, },
6: { 6: {
'f': do_shelf, "f": do_shelf,
'doc': 'Change shelf', "doc": "Change shelf",
}, },
9: { 9: {
'f': do_done, "f": do_done,
'doc': 'Done', "doc": "Done",
}, },
} }

@ -8,16 +8,17 @@ from worblehat.models import (
BookcaseShelf, BookcaseShelf,
) )
def select_bookcase_shelf( def select_bookcase_shelf(
bookcase: Bookcase, bookcase: Bookcase,
sql_session: Session, sql_session: Session,
prompt: str = "Please select the shelf where the item is placed (col-row):" prompt: str = "Please select the shelf where the item is placed (col-row):",
) -> BookcaseShelf: ) -> BookcaseShelf:
def __complete_bookshelf_selection(session: Session, cls: type, arg: str): def __complete_bookshelf_selection(session: Session, cls: type, arg: str):
args = arg.split('-') args = arg.split("-")
query = select(cls.row, cls.column).where(cls.bookcase == bookcase) query = select(cls.row, cls.column).where(cls.bookcase == bookcase)
try: try:
if arg != '' and len(args) > 0: if arg != "" and len(args) > 0:
query = query.where(cls.column == int(args[0])) query = query.where(cls.column == int(args[0]))
if len(args) > 1: if len(args) > 1:
query = query.where(cls.row == int(args[1])) query = query.where(cls.row == int(args[1]))
@ -25,21 +26,20 @@ def select_bookcase_shelf(
return [] return []
result = session.execute(query).all() result = session.execute(query).all()
return [f"{c}-{r}" for r,c in result] return [f"{c}-{r}" for r, c in result]
print(prompt) print(prompt)
bookcase_shelf_selector = InteractiveItemSelector( bookcase_shelf_selector = InteractiveItemSelector(
cls = BookcaseShelf, cls=BookcaseShelf,
sql_session = sql_session, sql_session=sql_session,
execute_selection = lambda session, cls, arg: session.scalars( execute_selection=lambda session, cls, arg: session.scalars(
select(cls) select(cls).where(
.where( cls.bookcase == bookcase,
cls.bookcase == bookcase, cls.column == int(arg.split("-")[0]),
cls.column == int(arg.split('-')[0]), cls.row == int(arg.split("-")[1]),
cls.row == int(arg.split('-')[1]),
) )
).all(), ).all(),
complete_selection = __complete_bookshelf_selection, complete_selection=__complete_bookshelf_selection,
) )
bookcase_shelf_selector.cmdloop() bookcase_shelf_selector.cmdloop()

@ -15,54 +15,51 @@ class SearchCli(NumberedCmd):
self.sql_session = sql_session self.sql_session = sql_session
self.result = None self.result = None
def do_search_all(self, _: str): def do_search_all(self, _: str):
print('TODO: Implement search all') print("TODO: Implement search all")
def do_search_title(self, _: str): def do_search_title(self, _: str):
while (input_text := input('Enter title: ')) == '': while (input_text := input("Enter title: ")) == "":
pass pass
items = self.sql_session.scalars( items = self.sql_session.scalars(
select(BookcaseItem) select(BookcaseItem).where(BookcaseItem.name.ilike(f"%{input_text}%")),
.where(BookcaseItem.name.ilike(f'%{input_text}%')),
).all() ).all()
if len(items) == 0: if len(items) == 0:
print('No items found.') print("No items found.")
return return
selector = NumberedItemSelector( selector = NumberedItemSelector(
items = items, items=items,
stringify = lambda item: f"{item.name} ({item.isbn})", stringify=lambda item: f"{item.name} ({item.isbn})",
) )
selector.cmdloop() selector.cmdloop()
if selector.result is not None: if selector.result is not None:
self.result = selector.result self.result = selector.result
return True return True
def do_search_author(self, _: str): def do_search_author(self, _: str):
while (input_text := input('Enter author name: ')) == '': while (input_text := input("Enter author name: ")) == "":
pass pass
author = self.sql_session.scalars( author = self.sql_session.scalars(
select(Author) select(Author).where(Author.name.ilike(f"%{input_text}%")),
.where(Author.name.ilike(f'%{input_text}%')),
).all() ).all()
if len(author) == 0: if len(author) == 0:
print('No authors found.') print("No authors found.")
return return
elif len(author) == 1: elif len(author) == 1:
selected_author = author[0] selected_author = author[0]
print('Found author:') print("Found author:")
print(f" {selected_author.name} ({sum(item.amount for item in selected_author.items)} items)") print(
f" {selected_author.name} ({sum(item.amount for item in selected_author.items)} items)"
)
else: else:
selector = NumberedItemSelector( selector = NumberedItemSelector(
items = author, items=author,
stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.items)} items)", stringify=lambda author: f"{author.name} ({sum(item.amount for item in author.items)} items)",
) )
selector.cmdloop() selector.cmdloop()
if selector.result is None: if selector.result is None:
@ -70,77 +67,73 @@ class SearchCli(NumberedCmd):
selected_author = selector.result selected_author = selector.result
selector = NumberedItemSelector( selector = NumberedItemSelector(
items = list(selected_author.items), items=list(selected_author.items),
stringify = lambda item: f"{item.name} ({item.isbn})", stringify=lambda item: f"{item.name} ({item.isbn})",
) )
selector.cmdloop() selector.cmdloop()
if selector.result is not None: if selector.result is not None:
self.result = selector.result self.result = selector.result
return True return True
def do_search_owner(self, _: str): def do_search_owner(self, _: str):
while (input_text := input('Enter username: ')) == '': while (input_text := input("Enter username: ")) == "":
pass pass
users = self.sql_session.scalars( users = self.sql_session.scalars(
select(BookcaseItem.owner) select(BookcaseItem.owner)
.where(BookcaseItem.owner.ilike(f'%{input_text}%')) .where(BookcaseItem.owner.ilike(f"%{input_text}%"))
.distinct(), .distinct(),
).all() ).all()
if len(users) == 0: if len(users) == 0:
print('No users found.') print("No users found.")
return return
elif len(users) == 1: elif len(users) == 1:
selected_user = users[0] selected_user = users[0]
print('Found user:') print("Found user:")
print(f" {selected_user}") print(f" {selected_user}")
else: else:
selector = NumberedItemSelector(items = users) selector = NumberedItemSelector(items=users)
selector.cmdloop() selector.cmdloop()
if selector.result is None: if selector.result is None:
return return
selected_user = selector.result selected_user = selector.result
items = self.sql_session.scalars( items = self.sql_session.scalars(
select(BookcaseItem) select(BookcaseItem).where(BookcaseItem.owner == selected_user),
.where(BookcaseItem.owner == selected_user),
).all() ).all()
selector = NumberedItemSelector( selector = NumberedItemSelector(
items = items, items=items,
stringify = lambda item: f"{item.name} ({item.isbn})", stringify=lambda item: f"{item.name} ({item.isbn})",
) )
selector.cmdloop() selector.cmdloop()
if selector.result is not None: if selector.result is not None:
self.result = selector.result self.result = selector.result
return True return True
def do_done(self, _: str): def do_done(self, _: str):
return True return True
funcs = { funcs = {
1: { 1: {
'f': do_search_all, "f": do_search_all,
'doc': 'Search everything', "doc": "Search everything",
}, },
2: { 2: {
'f': do_search_title, "f": do_search_title,
'doc': 'Search by title', "doc": "Search by title",
}, },
3: { 3: {
'f': do_search_author, "f": do_search_author,
'doc': 'Search by author', "doc": "Search by author",
}, },
4: { 4: {
'f': do_search_owner, "f": do_search_owner,
'doc': 'Search by owner', "doc": "Search by owner",
}, },
9: { 9: {
'f': do_done, "f": do_done,
'doc': 'Done', "doc": "Done",
}, },
} }

@ -1 +1,3 @@
from .main import DeadlineDaemon from .main import DeadlineDaemon
__all__ = ["DeadlineDaemon"]

@ -14,9 +14,10 @@ from worblehat.models import (
from worblehat.services.email import send_email from worblehat.services.email import send_email
class DeadlineDaemon: class DeadlineDaemon:
def __init__(self, sql_session: Session): def __init__(self, sql_session: Session):
if not Config['deadline_daemon.enabled']: if not Config["deadline_daemon.enabled"]:
return return
self.sql_session = sql_session self.sql_session = sql_session
@ -26,7 +27,7 @@ class DeadlineDaemon:
).one_or_none() ).one_or_none()
if self.last_run is None: if self.last_run is None:
logging.info('No previous run found, assuming this is the first run') logging.info("No previous run found, assuming this is the first run")
self.last_run = DeadlineDaemonLastRunDatetime(time=datetime.now()) self.last_run = DeadlineDaemonLastRunDatetime(time=datetime.now())
self.sql_session.add(self.last_run) self.sql_session.add(self.last_run)
self.sql_session.commit() self.sql_session.commit()
@ -34,15 +35,14 @@ class DeadlineDaemon:
self.last_run_datetime = self.last_run.time self.last_run_datetime = self.last_run.time
self.current_run_datetime = datetime.now() self.current_run_datetime = datetime.now()
def run(self): def run(self):
logging.info('Deadline daemon started') logging.info("Deadline daemon started")
if not Config['deadline_daemon.enabled']: if not Config["deadline_daemon.enabled"]:
logging.warn('Deadline daemon disabled, exiting') logging.warn("Deadline daemon disabled, exiting")
return return
if Config['deadline_daemon.dryrun']: if Config["deadline_daemon.dryrun"]:
logging.warn('Running in dryrun mode') logging.warn("Running in dryrun mode")
self.send_close_deadline_reminder_mails() self.send_close_deadline_reminder_mails()
self.send_overdue_mails() self.send_overdue_mails()
@ -58,78 +58,91 @@ class DeadlineDaemon:
################### ###################
def _send_close_deadline_mail(self, borrowing: BookcaseItemBorrowing): def _send_close_deadline_mail(self, borrowing: BookcaseItemBorrowing):
logging.info(f'Sending close deadline mail to {borrowing.username}@pvv.ntnu.no.') logging.info(
f"Sending close deadline mail to {borrowing.username}@pvv.ntnu.no."
)
send_email( send_email(
f'{borrowing.username}@pvv.ntnu.no', f"{borrowing.username}@pvv.ntnu.no",
'Reminder - Your borrowing deadline is approaching', "Reminder - Your borrowing deadline is approaching",
dedent(f''' dedent(
f"""
Your borrowing deadline for the following item is approaching: Your borrowing deadline for the following item is approaching:
{borrowing.item.name} {borrowing.item.name}
Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")} Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")}
''', """,
).strip(), ).strip(),
) )
def _send_overdue_mail(self, borrowing: BookcaseItemBorrowing): def _send_overdue_mail(self, borrowing: BookcaseItemBorrowing):
logging.info(f'Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime("%a %b %d, %Y")}') logging.info(
f"Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime('%a %b %d, %Y')}"
)
send_email( send_email(
f'{borrowing.username}@pvv.ntnu.no', f"{borrowing.username}@pvv.ntnu.no",
'Your deadline has passed', "Your deadline has passed",
dedent(f''' dedent(
f"""
Your delivery deadline for the following item has passed: Your delivery deadline for the following item has passed:
{borrowing.item.name} {borrowing.item.name}
Please return the item as soon as possible. Please return the item as soon as possible.
''', """,
).strip(), ).strip(),
) )
def _send_newly_available_mail(self, queue_item: BookcaseItemBorrowingQueue): def _send_newly_available_mail(self, queue_item: BookcaseItemBorrowingQueue):
logging.info(f'Sending newly available mail to {queue_item.username}') logging.info(f"Sending newly available mail to {queue_item.username}")
days_before_queue_expires = Config['deadline_daemon.days_before_queue_position_expires'] days_before_queue_expires = Config[
"deadline_daemon.days_before_queue_position_expires"
]
# TODO: calculate and format the date of when the queue position expires in the mail. # TODO: calculate and format the date of when the queue position expires in the mail.
send_email( send_email(
f'{queue_item.username}@pvv.ntnu.no', f"{queue_item.username}@pvv.ntnu.no",
'An item you have queued for is now available', "An item you have queued for is now available",
dedent(f''' dedent(
f"""
The following item is now available for you to borrow: The following item is now available for you to borrow:
{queue_item.item.name} {queue_item.item.name}
Please pick up the item within {days_before_queue_expires} days. Please pick up the item within {days_before_queue_expires} days.
''', """,
).strip(), ).strip(),
) )
def _send_expiring_queue_position_mail(
def _send_expiring_queue_position_mail(self, queue_position: BookcaseItemBorrowingQueue, day: int): self, queue_position: BookcaseItemBorrowingQueue, day: int
logging.info(f'Sending queue position expiry reminder to {queue_position.username}@pvv.ntnu.no.') ):
logging.info(
f"Sending queue position expiry reminder to {queue_position.username}@pvv.ntnu.no."
)
send_email( send_email(
f'{queue_position.username}@pvv.ntnu.no', f"{queue_position.username}@pvv.ntnu.no",
'Reminder - Your queue position expiry deadline is approaching', "Reminder - Your queue position expiry deadline is approaching",
dedent(f''' dedent(
f"""
Your queue position expiry deadline for the following item is approaching: Your queue position expiry deadline for the following item is approaching:
{queue_position.item.name} {queue_position.item.name}
Please borrow the item by {(queue_position.item_became_available_time + timedelta(days=day)).strftime("%a %b %d, %Y")} Please borrow the item by {(queue_position.item_became_available_time + timedelta(days=day)).strftime("%a %b %d, %Y")}
''', """,
).strip(), ).strip(),
) )
def _send_queue_position_expired_mail(
def _send_queue_position_expired_mail(self, queue_position: BookcaseItemBorrowingQueue): self, queue_position: BookcaseItemBorrowingQueue
):
send_email( send_email(
f'{queue_position.username}@pvv.ntnu.no', f"{queue_position.username}@pvv.ntnu.no",
'Your queue position has expired', "Your queue position has expired",
dedent(f''' dedent(
f"""
Your queue position for the following item has expired: Your queue position for the following item has expired:
{queue_position.item.name} {queue_position.item.name}
@ -137,7 +150,7 @@ class DeadlineDaemon:
You can queue for the item again at any time, but you will be placed at the back of the queue. You can queue for the item again at any time, but you will be placed at the back of the queue.
There are currently {len(queue_position.item.borrowing_queue)} users in the queue. There are currently {len(queue_position.item.borrowing_queue)} users in the queue.
''', """,
).strip(), ).strip(),
) )
@ -146,30 +159,32 @@ class DeadlineDaemon:
################## ##################
def _sql_subtract_date(self, x: datetime, y: timedelta): def _sql_subtract_date(self, x: datetime, y: timedelta):
if self.sql_session.bind.dialect.name == 'sqlite': if self.sql_session.bind.dialect.name == "sqlite":
# SQLite does not support timedelta in queries # SQLite does not support timedelta in queries
return func.datetime(x, f'-{y.days} days') return func.datetime(x, f"-{y.days} days")
elif self.sql_session.bind.dialect.name == 'postgresql': elif self.sql_session.bind.dialect.name == "postgresql":
return x - y return x - y
else: else:
raise NotImplementedError(f'Unsupported dialect: {self.sql_session.bind.dialect.name}') raise NotImplementedError(
f"Unsupported dialect: {self.sql_session.bind.dialect.name}"
)
def send_close_deadline_reminder_mails(self): def send_close_deadline_reminder_mails(self):
logging.info('Sending mails for items with a closing deadline') logging.info("Sending mails for items with a closing deadline")
# TODO: This should be int-parsed and validated before the daemon started # TODO: This should be int-parsed and validated before the daemon started
days = [int(d) for d in Config['deadline_daemon.warn_days_before_borrowing_deadline']] days = [
int(d)
for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]
]
for day in days: for day in days:
borrowings_to_remind = self.sql_session.scalars( borrowings_to_remind = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing).where(
.where(
self._sql_subtract_date( self._sql_subtract_date(
BookcaseItemBorrowing.end_time, BookcaseItemBorrowing.end_time,
timedelta(days=day), timedelta(days=day),
) ).between(
.between(
self.last_run_datetime, self.last_run_datetime,
self.current_run_datetime, self.current_run_datetime,
), ),
@ -179,13 +194,11 @@ class DeadlineDaemon:
for borrowing in borrowings_to_remind: for borrowing in borrowings_to_remind:
self._send_close_deadline_mail(borrowing) self._send_close_deadline_mail(borrowing)
def send_overdue_mails(self): def send_overdue_mails(self):
logging.info('Sending mails for overdue items') logging.info("Sending mails for overdue items")
to_remind = self.sql_session.scalars( to_remind = self.sql_session.scalars(
select(BookcaseItemBorrowing) select(BookcaseItemBorrowing).where(
.where(
BookcaseItemBorrowing.end_time < self.current_run_datetime, BookcaseItemBorrowing.end_time < self.current_run_datetime,
BookcaseItemBorrowing.delivered.is_(None), BookcaseItemBorrowing.delivered.is_(None),
) )
@ -194,15 +207,15 @@ class DeadlineDaemon:
for borrowing in to_remind: for borrowing in to_remind:
self._send_overdue_mail(borrowing) self._send_overdue_mail(borrowing)
def send_newly_available_mails(self): def send_newly_available_mails(self):
logging.info('Sending mails about newly available items') logging.info("Sending mails about newly available items")
newly_available = self.sql_session.scalars( newly_available = self.sql_session.scalars(
select(BookcaseItemBorrowingQueue) select(BookcaseItemBorrowingQueue)
.join( .join(
BookcaseItemBorrowing, BookcaseItemBorrowing,
BookcaseItemBorrowing.fk_bookcase_item_uid == BookcaseItemBorrowingQueue.fk_bookcase_item_uid, BookcaseItemBorrowing.fk_bookcase_item_uid
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
) )
.where( .where(
BookcaseItemBorrowingQueue.expired.is_(False), BookcaseItemBorrowingQueue.expired.is_(False),
@ -217,31 +230,38 @@ class DeadlineDaemon:
).all() ).all()
for queue_item in newly_available: for queue_item in newly_available:
logging.info(f'Adding user {queue_item.username} to queue for {queue_item.item.name}') logging.info(
f"Adding user {queue_item.username} to queue for {queue_item.item.name}"
)
queue_item.item_became_available_time = self.current_run_datetime queue_item.item_became_available_time = self.current_run_datetime
self.sql_session.commit() self.sql_session.commit()
self._send_newly_available_mail(queue_item) self._send_newly_available_mail(queue_item)
def send_expiring_queue_position_mails(self): def send_expiring_queue_position_mails(self):
logging.info('Sending mails about queue positions which are expiring soon') logging.info("Sending mails about queue positions which are expiring soon")
logging.warning('Not implemented') logging.warning("Not implemented")
days = [int(d) for d in Config['deadline_daemon.warn_days_before_expiring_queue_position_deadline']] days = [
int(d)
for d in Config[
"deadline_daemon.warn_days_before_expiring_queue_position_deadline"
]
]
for day in days: for day in days:
queue_positions_to_remind = self.sql_session.scalars( queue_positions_to_remind = self.sql_session.scalars(
select(BookcaseItemBorrowingQueue) select(BookcaseItemBorrowingQueue)
.join( .join(
BookcaseItemBorrowing, BookcaseItemBorrowing,
BookcaseItemBorrowing.fk_bookcase_item_uid == BookcaseItemBorrowingQueue.fk_bookcase_item_uid, BookcaseItemBorrowing.fk_bookcase_item_uid
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
) )
.where( .where(
self._sql_subtract_date( self._sql_subtract_date(
BookcaseItemBorrowingQueue.item_became_available_time + timedelta(days=day), BookcaseItemBorrowingQueue.item_became_available_time
+ timedelta(days=day),
timedelta(days=day), timedelta(days=day),
) ).between(
.between(
self.last_run_datetime, self.last_run_datetime,
self.current_run_datetime, self.current_run_datetime,
), ),
@ -251,29 +271,34 @@ class DeadlineDaemon:
for queue_position in queue_positions_to_remind: for queue_position in queue_positions_to_remind:
self._send_expiring_queue_position_mail(queue_position, day) self._send_expiring_queue_position_mail(queue_position, day)
def auto_expire_queue_positions(self): def auto_expire_queue_positions(self):
logging.info('Expiring queue positions which are too old') logging.info("Expiring queue positions which are too old")
queue_position_expiry_days = int(Config['deadline_daemon.days_before_queue_position_expires']) queue_position_expiry_days = int(
Config["deadline_daemon.days_before_queue_position_expires"]
)
overdue_queue_positions = self.sql_session.scalars( overdue_queue_positions = self.sql_session.scalars(
select(BookcaseItemBorrowingQueue) select(BookcaseItemBorrowingQueue).where(
.where( BookcaseItemBorrowingQueue.item_became_available_time
BookcaseItemBorrowingQueue.item_became_available_time + timedelta(days=queue_position_expiry_days) < self.current_run_datetime, + timedelta(days=queue_position_expiry_days)
< self.current_run_datetime,
BookcaseItemBorrowingQueue.expired.is_(False), BookcaseItemBorrowingQueue.expired.is_(False),
), ),
).all() ).all()
for queue_position in overdue_queue_positions: for queue_position in overdue_queue_positions:
logging.info(f'Expiring queue position for {queue_position.username} for item {queue_position.item.name}') logging.info(
f"Expiring queue position for {queue_position.username} for item {queue_position.item.name}"
)
queue_position.expired = True queue_position.expired = True
next_queue_position = self.sql_session.scalars( next_queue_position = self.sql_session.scalars(
select(BookcaseItemBorrowingQueue) select(BookcaseItemBorrowingQueue)
.where( .where(
BookcaseItemBorrowingQueue.fk_bookcase_item_uid == queue_position.fk_bookcase_item_uid, BookcaseItemBorrowingQueue.fk_bookcase_item_uid
== queue_position.fk_bookcase_item_uid,
BookcaseItemBorrowingQueue.item_became_available_time.is_(None), BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
) )
.order_by(BookcaseItemBorrowingQueue.entered_queue_time) .order_by(BookcaseItemBorrowingQueue.entered_queue_time)
@ -283,9 +308,13 @@ class DeadlineDaemon:
self._send_queue_position_expired_mail(queue_position) self._send_queue_position_expired_mail(queue_position)
if next_queue_position is not None: if next_queue_position is not None:
next_queue_position.item_became_available_time = self.current_run_datetime next_queue_position.item_became_available_time = (
self.current_run_datetime
)
logging.info(f'Next user in queue for item {next_queue_position.item.name} is {next_queue_position.username}') logging.info(
f"Next user in queue for item {next_queue_position.item.name} is {next_queue_position.username}"
)
self._send_newly_available_mail(next_queue_position) self._send_newly_available_mail(next_queue_position)
self.sql_session.commit() self.sql_session.commit()

@ -1,10 +1,10 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from worblehat.models import ( from worblehat.models import (
BookcaseItem, BookcaseItem,
BookcaseItemBorrowing, BookcaseItemBorrowing,
BookcaseItemBorrowingQueue, BookcaseItemBorrowingQueue,
DeadlineDaemonLastRunDatetime, DeadlineDaemonLastRunDatetime,
) )
from worblehat.services.config import Config from worblehat.services.config import Config
@ -13,105 +13,115 @@ from .seed_test_data import main as seed_test_data_main
def clear_db(sql_session): def clear_db(sql_session):
sql_session.query(BookcaseItemBorrowingQueue).delete() sql_session.query(BookcaseItemBorrowingQueue).delete()
sql_session.query(BookcaseItemBorrowing).delete() sql_session.query(BookcaseItemBorrowing).delete()
sql_session.query(DeadlineDaemonLastRunDatetime).delete() sql_session.query(DeadlineDaemonLastRunDatetime).delete()
sql_session.commit() sql_session.commit()
# NOTE: feel free to change this function to suit your needs # NOTE: feel free to change this function to suit your needs
# it's just a quick and dirty way to get some data into the database # it's just a quick and dirty way to get some data into the database
# for testing the deadline daemon - oysteikt 2024 # for testing the deadline daemon - oysteikt 2024
def main(sql_session): def main(sql_session):
borrow_warning_days = [timedelta(days=int(d)) for d in Config['deadline_daemon.warn_days_before_borrowing_deadline']] borrow_warning_days = [
queue_warning_days = [timedelta(days=int(d)) for d in Config['deadline_daemon.warn_days_before_expiring_queue_position_deadline']] timedelta(days=int(d))
queue_expire_days = int(Config['deadline_daemon.days_before_queue_position_expires']) for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]
]
clear_db(sql_session) queue_warning_days = [
seed_test_data_main(sql_session) timedelta(days=int(d))
for d in Config[
books = sql_session.query(BookcaseItem).all() "deadline_daemon.warn_days_before_expiring_queue_position_deadline"
]
last_run_datetime = datetime.now() - timedelta(days=16) ]
last_run = DeadlineDaemonLastRunDatetime(last_run_datetime) queue_expire_days = int(
sql_session.add(last_run) Config["deadline_daemon.days_before_queue_position_expires"]
# Create at least one item that is borrowed and not supposed to be returned yet
borrowing = BookcaseItemBorrowing(
item=books[0],
username='test_borrower_still_borrowing',
)
borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() - timedelta(days=6)
sql_session.add(borrowing)
# Create at least one item that is borrowed and is supposed to be returned soon
borrowing = BookcaseItemBorrowing(
item=books[1],
username='test_borrower_return_soon',
)
borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() - timedelta(days=2)
sql_session.add(borrowing)
# Create at least one item that is borrowed and is overdue
borrowing = BookcaseItemBorrowing(
item=books[2],
username='test_borrower_overdue',
)
borrowing.start_time = datetime.now() - timedelta(days=1)
borrowing.end_time = datetime.now() + timedelta(days=1)
sql_session.add(borrowing)
# Create at least one item that is in the queue and is not supposed to be borrowed yet
queue_item = BookcaseItemBorrowingQueue(
item=books[3],
username='test_queue_user_still_waiting',
)
queue_item.entered_queue_time = last_run_datetime - timedelta(days=1)
borrowing = BookcaseItemBorrowing(
item=books[3],
username='test_borrower_return_soon',
)
borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() - timedelta(days=2)
sql_session.add(queue_item)
sql_session.add(borrowing)
# Create at least three items that is in the queue and two items were just returned
for i in range(3):
queue_item = BookcaseItemBorrowingQueue(
item=books[4 + i],
username=f'test_queue_user_{i}',
) )
sql_session.add(queue_item)
for i in range(3): clear_db(sql_session)
seed_test_data_main(sql_session)
books = sql_session.query(BookcaseItem).all()
last_run_datetime = datetime.now() - timedelta(days=16)
last_run = DeadlineDaemonLastRunDatetime(last_run_datetime)
sql_session.add(last_run)
# Create at least one item that is borrowed and not supposed to be returned yet
borrowing = BookcaseItemBorrowing( borrowing = BookcaseItemBorrowing(
item=books[4 + i], item=books[0],
username=f'test_borrower_returned_{i}', username="test_borrower_still_borrowing",
) )
borrowing.start_time = last_run_datetime - timedelta(days=2) borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() + timedelta(days=1) borrowing.end_time = datetime.now() - timedelta(days=6)
if i != 2:
borrowing.delivered = datetime.now() - timedelta(days=1)
sql_session.add(borrowing) sql_session.add(borrowing)
# Create at least one item that has been in the queue for so long that the queue position should expire # Create at least one item that is borrowed and is supposed to be returned soon
queue_item = BookcaseItemBorrowingQueue( borrowing = BookcaseItemBorrowing(
item=books[7], item=books[1],
username='test_queue_user_expired', username="test_borrower_return_soon",
) )
queue_item.entered_queue_time = datetime.now() - timedelta(days=15) borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() - timedelta(days=2)
sql_session.add(borrowing)
# Create at least one item that has been in the queue for so long that the queue position should expire, # Create at least one item that is borrowed and is overdue
# but the queue person has already been notified borrowing = BookcaseItemBorrowing(
queue_item = BookcaseItemBorrowingQueue( item=books[2],
item=books[8], username="test_borrower_overdue",
username='test_queue_user_expired_notified', )
) borrowing.start_time = datetime.now() - timedelta(days=1)
queue_item.entered_queue_time = datetime.now() - timedelta(days=15) borrowing.end_time = datetime.now() + timedelta(days=1)
sql_session.add(borrowing)
sql_session.commit() # Create at least one item that is in the queue and is not supposed to be borrowed yet
queue_item = BookcaseItemBorrowingQueue(
item=books[3],
username="test_queue_user_still_waiting",
)
queue_item.entered_queue_time = last_run_datetime - timedelta(days=1)
borrowing = BookcaseItemBorrowing(
item=books[3],
username="test_borrower_return_soon",
)
borrowing.start_time = last_run_datetime - timedelta(days=1)
borrowing.end_time = datetime.now() - timedelta(days=2)
sql_session.add(queue_item)
sql_session.add(borrowing)
# Create at least three items that is in the queue and two items were just returned
for i in range(3):
queue_item = BookcaseItemBorrowingQueue(
item=books[4 + i],
username=f"test_queue_user_{i}",
)
sql_session.add(queue_item)
for i in range(3):
borrowing = BookcaseItemBorrowing(
item=books[4 + i],
username=f"test_borrower_returned_{i}",
)
borrowing.start_time = last_run_datetime - timedelta(days=2)
borrowing.end_time = datetime.now() + timedelta(days=1)
if i != 2:
borrowing.delivered = datetime.now() - timedelta(days=1)
sql_session.add(borrowing)
# Create at least one item that has been in the queue for so long that the queue position should expire
queue_item = BookcaseItemBorrowingQueue(
item=books[7],
username="test_queue_user_expired",
)
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
# Create at least one item that has been in the queue for so long that the queue position should expire,
# but the queue person has already been notified
queue_item = BookcaseItemBorrowingQueue(
item=books[8],
username="test_queue_user_expired_notified",
)
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
sql_session.commit()

@ -1,19 +1,18 @@
import csv import csv
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta
from worblehat.models import ( from worblehat.models import (
Bookcase, Bookcase,
BookcaseItem, BookcaseItem,
BookcaseShelf, BookcaseShelf,
MediaType, MediaType,
Language, Language,
) )
from worblehat.services.config import Config
CSV_FILE = Path(__file__).parent.parent.parent / 'data' / 'arbeidsrom_smal_hylle_5.csv' CSV_FILE = Path(__file__).parent.parent.parent / "data" / "arbeidsrom_smal_hylle_5.csv"
def clear_db(sql_session): def clear_db(sql_session):
sql_session.query(BookcaseItem).delete() sql_session.query(BookcaseItem).delete()
@ -23,45 +22,46 @@ def clear_db(sql_session):
sql_session.query(Language).delete() sql_session.query(Language).delete()
sql_session.commit() sql_session.commit()
def main(sql_session): def main(sql_session):
clear_db(sql_session) clear_db(sql_session)
media_type = MediaType( media_type = MediaType(
name='Book', name="Book",
description='A book', description="A book",
) )
sql_session.add(media_type) sql_session.add(media_type)
language = Language( language = Language(
name='Norwegian', name="Norwegian",
iso639_1_code='no', iso639_1_code="no",
) )
sql_session.add(language) sql_session.add(language)
seed_case = Bookcase( seed_case = Bookcase(
name='seed_case', name="seed_case",
description='test bookcase with test data', description="test bookcase with test data",
) )
sql_session.add(seed_case) sql_session.add(seed_case)
seed_shelf_1 = BookcaseShelf( seed_shelf_1 = BookcaseShelf(
row=1, row=1,
column=1, column=1,
bookcase=seed_case, bookcase=seed_case,
description='test shelf with test data 1', description="test shelf with test data 1",
) )
seed_shelf_2 = BookcaseShelf( seed_shelf_2 = BookcaseShelf(
row=2, row=2,
column=1, column=1,
bookcase=seed_case, bookcase=seed_case,
description='test shelf with test data 2', description="test shelf with test data 2",
) )
sql_session.add(seed_shelf_1) sql_session.add(seed_shelf_1)
sql_session.add(seed_shelf_2) sql_session.add(seed_shelf_2)
bookcase_items = [] bookcase_items = []
with open(CSV_FILE) as csv_file: with open(CSV_FILE) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',') csv_reader = csv.reader(csv_file, delimiter=",")
next(csv_reader) next(csv_reader)
for row in csv_reader: for row in csv_reader:

@ -2,10 +2,12 @@ from flask import Blueprint, render_template
main = Blueprint("main", __name__, template_folder="main") main = Blueprint("main", __name__, template_folder="main")
@main.route('/')
@main.route("/")
def index(): def index():
return render_template("main/index.html") return render_template("main/index.html")
@main.route("/login") @main.route("/login")
def login(): def login():
return render_template("main/login.html") return render_template("main/login.html")

@ -1,3 +1,3 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() db = SQLAlchemy()

@ -10,18 +10,19 @@ from worblehat.services.config import Config
from .blueprints.main import main from .blueprints.main import main
from .database import db from .database import db
def create_app(args: dict[str, any] | None = None): def create_app(args: dict[str, any] | None = None):
app = Flask(__name__) app = Flask(__name__)
app.config.update(Config['flask']) app.config.update(Config["flask"])
app.config.update(Config._config) app.config.update(Config._config)
app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string() app.config["SQLALCHEMY_DATABASE_URI"] = Config.db_string()
app.config['SQLALCHEMY_ECHO'] = Config['logging.debug_sql'] app.config["SQLALCHEMY_ECHO"] = Config["logging.debug_sql"]
db.init_app(app) db.init_app(app)
with app.app_context(): with app.app_context():
if not inspect(db.engine).has_table('Bookcase'): if not inspect(db.engine).has_table("Bookcase"):
Base.metadata.create_all(db.engine) Base.metadata.create_all(db.engine)
seed_data() seed_data()
@ -31,12 +32,13 @@ def create_app(args: dict[str, any] | None = None):
return app return app
def configure_admin(app): def configure_admin(app):
admin = Admin(app, name='Worblehat', template_mode='bootstrap3') admin = Admin(app, name="Worblehat", template_mode="bootstrap3")
admin.add_view(ModelView(Author, db.session)) admin.add_view(ModelView(Author, db.session))
admin.add_view(ModelView(Bookcase, db.session)) admin.add_view(ModelView(Bookcase, db.session))
admin.add_view(ModelView(BookcaseItem, db.session)) admin.add_view(ModelView(BookcaseItem, db.session))
admin.add_view(ModelView(BookcaseShelf, db.session)) admin.add_view(ModelView(BookcaseShelf, db.session))
admin.add_view(ModelView(Category, db.session)) admin.add_view(ModelView(Category, db.session))
admin.add_view(ModelView(Language, db.session)) admin.add_view(ModelView(Language, db.session))
admin.add_view(ModelView(MediaType, db.session)) admin.add_view(ModelView(MediaType, db.session))

@ -1,18 +1,19 @@
from werkzeug import run_simple from werkzeug import run_simple
from worblehat.services.config import Config
from .flaskapp import create_app from .flaskapp import create_app
def main(): def main():
app = create_app() app = create_app()
run_simple( run_simple(
hostname = 'localhost', hostname="localhost",
port = 5000, port=5000,
application = app, application=app,
use_debugger = True, use_debugger=True,
use_reloader = True, use_reloader=True,
) )
if __name__ == '__main__':
main() if __name__ == "__main__":
main()

@ -1,8 +1,10 @@
from .flaskapp import create_app from .flaskapp import create_app
def main(): def main():
app = create_app() app = create_app()
app.run() app.run()
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

@ -18,7 +18,8 @@ from .flaskapp.wsgi_prod import main as flask_prod_main
def _print_version() -> None: def _print_version() -> None:
from worblehat import __version__ from worblehat import __version__
print(f'Worblehat version {__version__}')
print(f"Worblehat version {__version__}")
def _connect_to_database(**engine_args) -> Session: def _connect_to_database(**engine_args) -> Session:
@ -26,7 +27,7 @@ def _connect_to_database(**engine_args) -> Session:
engine = create_engine(Config.db_string(), **engine_args) engine = create_engine(Config.db_string(), **engine_args)
sql_session = Session(engine) sql_session = Session(engine)
except Exception as err: except Exception as err:
print('Error: could not connect to database.') print("Error: could not connect to database.")
print(err) print(err)
exit(1) exit(1)
@ -38,51 +39,55 @@ def main():
args = arg_parser.parse_args() args = arg_parser.parse_args()
Config.load_configuration(vars(args)) Config.load_configuration(vars(args))
if Config['logging.debug']: if Config["logging.debug"]:
logging.basicConfig(encoding='utf-8', level=logging.DEBUG) logging.basicConfig(encoding="utf-8", level=logging.DEBUG)
else: else:
logging.basicConfig(encoding='utf-8', level=logging.INFO) logging.basicConfig(encoding="utf-8", level=logging.INFO)
if args.version: if args.version:
_print_version() _print_version()
exit(0) exit(0)
if args.print_config: if args.print_config:
print(f'Configuration:\n{pformat(vars(args))}') print(f"Configuration:\n{pformat(vars(args))}")
exit(0) exit(0)
if args.command == 'deadline-daemon': if args.command == "deadline-daemon":
sql_session = _connect_to_database(echo=Config['logging.debug_sql']) sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
DeadlineDaemon(sql_session).run() DeadlineDaemon(sql_session).run()
exit(0) exit(0)
if args.command == 'cli': if args.command == "cli":
sql_session = _connect_to_database(echo=Config['logging.debug_sql']) sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
WorblehatCli.run_with_safe_exit_wrapper(sql_session) WorblehatCli.run_with_safe_exit_wrapper(sql_session)
exit(0) exit(0)
if args.command == 'devscripts': if args.command == "devscripts":
sql_session = _connect_to_database(echo=Config['logging.debug_sql']) sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
if args.script == 'seed-content-for-deadline-daemon': if args.script == "seed-content-for-deadline-daemon":
from .devscripts.seed_content_for_deadline_daemon import main from .devscripts.seed_content_for_deadline_daemon import main
main(sql_session) main(sql_session)
elif args.script == 'seed-test-data': elif args.script == "seed-test-data":
from .devscripts.seed_test_data import main from .devscripts.seed_test_data import main
main(sql_session) main(sql_session)
else: else:
print(devscripts_arg_parser.format_help()) print(devscripts_arg_parser.format_help())
exit(1) exit(1)
exit(0) exit(0)
if args.command == 'flask-dev': if args.command == "flask-dev":
flask_dev_main() flask_dev_main()
exit(0) exit(0)
if args.command == 'flask-prod': if args.command == "flask-prod":
if Config['logging.debug'] or Config['logging.debug_sql']: if Config["logging.debug"] or Config["logging.debug_sql"]:
logging.warn('Debug mode is enabled for the production server. This is not recommended.') logging.warn(
"Debug mode is enabled for the production server. This is not recommended."
)
flask_prod_main() flask_prod_main()
exit(0) exit(0)
print(arg_parser.format_help()) print(arg_parser.format_help())
exit(1) exit(1)

@ -1,13 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import (
Integer,
ForeignKey,
)
from sqlalchemy.orm import ( from sqlalchemy.orm import (
Mapped, Mapped,
mapped_column,
relationship, relationship,
) )
@ -21,14 +16,15 @@ from .xref_tables import Item_Author
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
class Author(Base, UidMixin, UniqueNameMixin): class Author(Base, UidMixin, UniqueNameMixin):
items: Mapped[set[BookcaseItem]] = relationship( items: Mapped[set[BookcaseItem]] = relationship(
secondary = Item_Author.__table__, secondary=Item_Author.__table__,
back_populates = 'authors', back_populates="authors",
) )
def __init__( def __init__(
self, self,
name: str, name: str,
): ):
self.name = name self.name = name

@ -9,6 +9,7 @@ from sqlalchemy.orm.collections import (
InstrumentedSet, InstrumentedSet,
) )
class Base(DeclarativeBase): class Base(DeclarativeBase):
metadata = MetaData( metadata = MetaData(
naming_convention={ naming_convention={
@ -16,7 +17,7 @@ class Base(DeclarativeBase):
"uq": "uq_%(table_name)s_%(column_0_name)s", "uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_`%(constraint_name)s`", "ck": "ck_%(table_name)s_`%(constraint_name)s`",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s" "pk": "pk_%(table_name)s",
} }
) )
@ -26,15 +27,18 @@ class Base(DeclarativeBase):
def __repr__(self) -> str: def __repr__(self) -> str:
columns = ", ".join( columns = ", ".join(
f"{k}={repr(v)}" for k, v in self.__dict__.items() if not any([ f"{k}={repr(v)}"
k.startswith("_"), for k, v in self.__dict__.items()
if not any(
# Ensure that we don't try to print out the entire list of [
# relationships, which could create an infinite loop k.startswith("_"),
isinstance(v, Base), # Ensure that we don't try to print out the entire list of
isinstance(v, InstrumentedList), # relationships, which could create an infinite loop
isinstance(v, InstrumentedSet), isinstance(v, Base),
isinstance(v, InstrumentedDict), isinstance(v, InstrumentedList),
]) isinstance(v, InstrumentedSet),
isinstance(v, InstrumentedDict),
]
)
) )
return f"<{self.__class__.__name__}({columns})>" return f"<{self.__class__.__name__}({columns})>"

@ -13,13 +13,15 @@ from .mixins import (
UidMixin, UidMixin,
UniqueNameMixin, UniqueNameMixin,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseShelf import BookcaseShelf from .BookcaseShelf import BookcaseShelf
class Bookcase(Base, UidMixin, UniqueNameMixin): class Bookcase(Base, UidMixin, UniqueNameMixin):
description: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
shelfs: Mapped[list[BookcaseShelf]] = relationship(back_populates='bookcase') shelfs: Mapped[list[BookcaseShelf]] = relationship(back_populates="bookcase")
def __init__( def __init__(
self, self,
@ -32,6 +34,5 @@ class Bookcase(Base, UidMixin, UniqueNameMixin):
def short_str(self) -> str: def short_str(self) -> str:
result = self.name result = self.name
if self.description is not None: if self.description is not None:
result += f' [{self.description}]' result += f" [{self.description}]"
return result return result

@ -2,11 +2,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import ( from sqlalchemy import (
ForeignKey, ForeignKey,
Integer, SmallInteger,
SmallInteger, String,
String, Text,
Text,
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import (
Mapped, Mapped,
@ -17,12 +16,12 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import ( from .mixins import (
UidMixin, UidMixin,
UniqueNameMixin,
) )
from .xref_tables import ( from .xref_tables import (
Item_Category, Item_Category,
Item_Author, Item_Author,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from .Author import Author from .Author import Author
from .BookcaseItemBorrowing import BookcaseItemBorrowing from .BookcaseItemBorrowing import BookcaseItemBorrowing
@ -34,36 +33,39 @@ if TYPE_CHECKING:
from worblehat.flaskapp.database import db from worblehat.flaskapp.database import db
class BookcaseItem(Base, UidMixin): class BookcaseItem(Base, UidMixin):
isbn: Mapped[int] = mapped_column(String, unique=True, index=True) isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
name: Mapped[str] = mapped_column(Text, index=True) name: Mapped[str] = mapped_column(Text, index=True)
owner: Mapped[str] = mapped_column(String, default='PVV') owner: Mapped[str] = mapped_column(String, default="PVV")
amount: Mapped[int] = mapped_column(SmallInteger, default=1) amount: Mapped[int] = mapped_column(SmallInteger, default=1)
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey('MediaType.uid')) fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey("MediaType.uid"))
fk_bookcase_shelf_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseShelf.uid')) fk_bookcase_shelf_uid: Mapped[int] = mapped_column(ForeignKey("BookcaseShelf.uid"))
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey('Language.uid')) fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey("Language.uid"))
media_type: Mapped[MediaType] = relationship(back_populates='items') media_type: Mapped[MediaType] = relationship(back_populates="items")
shelf: Mapped[BookcaseShelf] = relationship(back_populates='items') shelf: Mapped[BookcaseShelf] = relationship(back_populates="items")
language: Mapped[Language] = relationship() language: Mapped[Language] = relationship()
borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates='item') borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates="item")
borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(back_populates='item') borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(
back_populates="item"
)
categories: Mapped[set[Category]] = relationship( categories: Mapped[set[Category]] = relationship(
secondary = Item_Category.__table__, secondary=Item_Category.__table__,
back_populates = 'items', back_populates="items",
) )
authors: Mapped[set[Author]] = relationship( authors: Mapped[set[Author]] = relationship(
secondary = Item_Author.__table__, secondary=Item_Author.__table__,
back_populates = 'items', back_populates="items",
) )
def __init__( def __init__(
self, self,
name: str, name: str,
isbn: int | None = None, isbn: int | None = None,
owner: str = 'PVV', owner: str = "PVV",
): ):
self.name = name self.name = name
self.isbn = isbn self.isbn = isbn
@ -76,4 +78,4 @@ class BookcaseItem(Base, UidMixin):
This method defaults to using the flask_sqlalchemy session. This method defaults to using the flask_sqlalchemy session.
It will not work outside of a request context, unless another session is provided. It will not work outside of a request context, unless another session is provided.
""" """
return sql_session.query(cls).where(cls.isbn == isbn).one_or_none() return sql_session.query(cls).where(cls.isbn == isbn).one_or_none()

@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import ( from sqlalchemy import (
Boolean,
ForeignKey, ForeignKey,
String, String,
DateTime, DateTime,
@ -16,18 +15,24 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import UidMixin from .mixins import UidMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
class BookcaseItemBorrowing(Base, UidMixin): class BookcaseItemBorrowing(Base, UidMixin):
username: Mapped[str] = mapped_column(String) username: Mapped[str] = mapped_column(String)
start_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now()) start_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
end_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now() + timedelta(days=30)) end_time: Mapped[datetime] = mapped_column(
DateTime, default=datetime.now() + timedelta(days=30)
)
delivered: Mapped[datetime | None] = mapped_column(DateTime, default=None) delivered: Mapped[datetime | None] = mapped_column(DateTime, default=None)
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True) fk_bookcase_item_uid: Mapped[int] = mapped_column(
ForeignKey("BookcaseItem.uid"), index=True
)
item: Mapped[BookcaseItem] = relationship(back_populates='borrowings') item: Mapped[BookcaseItem] = relationship(back_populates="borrowings")
def __init__( def __init__(
self, self,
@ -37,4 +42,4 @@ class BookcaseItemBorrowing(Base, UidMixin):
self.username = username self.username = username
self.item = item self.item = item
self.start_time = datetime.now() self.start_time = datetime.now()
self.end_time = datetime.now() + timedelta(days=30) self.end_time = datetime.now() + timedelta(days=30)

@ -16,18 +16,24 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import UidMixin from .mixins import UidMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
class BookcaseItemBorrowingQueue(Base, UidMixin): class BookcaseItemBorrowingQueue(Base, UidMixin):
username: Mapped[str] = mapped_column(String) username: Mapped[str] = mapped_column(String)
entered_queue_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now()) entered_queue_time: Mapped[datetime] = mapped_column(
DateTime, default=datetime.now()
)
item_became_available_time: Mapped[datetime | None] = mapped_column(DateTime) item_became_available_time: Mapped[datetime | None] = mapped_column(DateTime)
expired = mapped_column(Boolean, default=False) expired = mapped_column(Boolean, default=False)
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True) fk_bookcase_item_uid: Mapped[int] = mapped_column(
ForeignKey("BookcaseItem.uid"), index=True
)
item: Mapped[BookcaseItem] = relationship(back_populates='borrowing_queue') item: Mapped[BookcaseItem] = relationship(back_populates="borrowing_queue")
def __init__( def __init__(
self, self,
@ -36,4 +42,4 @@ class BookcaseItemBorrowingQueue(Base, UidMixin):
): ):
self.username = username self.username = username
self.item = item self.item = item
self.entered_queue_time = datetime.now() self.entered_queue_time = datetime.now()

@ -2,7 +2,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import ( from sqlalchemy import (
Integer,
ForeignKey, ForeignKey,
SmallInteger, SmallInteger,
Text, Text,
@ -16,6 +15,7 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import UidMixin from .mixins import UidMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from .Bookcase import Bookcase from .Bookcase import Bookcase
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
@ -23,22 +23,23 @@ if TYPE_CHECKING:
# NOTE: Booshelfs are 0 indexed for both rows and columns, # NOTE: Booshelfs are 0 indexed for both rows and columns,
# where cell 0-0 is placed in the lower right corner. # where cell 0-0 is placed in the lower right corner.
class BookcaseShelf(Base, UidMixin): class BookcaseShelf(Base, UidMixin):
__table_args__ = ( __table_args__ = (
UniqueConstraint( UniqueConstraint(
'column', "column",
'fk_bookcase_uid', "fk_bookcase_uid",
'row', "row",
), ),
) )
description: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
row: Mapped[int] = mapped_column(SmallInteger) row: Mapped[int] = mapped_column(SmallInteger)
column: Mapped[int] = mapped_column(SmallInteger) column: Mapped[int] = mapped_column(SmallInteger)
fk_bookcase_uid: Mapped[int] = mapped_column(ForeignKey('Bookcase.uid')) fk_bookcase_uid: Mapped[int] = mapped_column(ForeignKey("Bookcase.uid"))
bookcase: Mapped[Bookcase] = relationship(back_populates='shelfs') bookcase: Mapped[Bookcase] = relationship(back_populates="shelfs")
items: Mapped[set[BookcaseItem]] = relationship(back_populates='shelf') items: Mapped[set[BookcaseItem]] = relationship(back_populates="shelf")
def __init__( def __init__(
self, self,
@ -53,7 +54,7 @@ class BookcaseShelf(Base, UidMixin):
self.description = description self.description = description
def short_str(self) -> str: def short_str(self) -> str:
result = f'{self.column}-{self.row}' result = f"{self.column}-{self.row}"
if self.description is not None: if self.description is not None:
result += f' [{self.description}]' result += f" [{self.description}]"
return result return result

@ -14,15 +14,17 @@ from .mixins import (
UniqueNameMixin, UniqueNameMixin,
) )
from .xref_tables import Item_Category from .xref_tables import Item_Category
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
class Category(Base, UidMixin, UniqueNameMixin): class Category(Base, UidMixin, UniqueNameMixin):
description: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
items: Mapped[set[BookcaseItem]] = relationship( items: Mapped[set[BookcaseItem]] = relationship(
secondary=Item_Category.__table__, secondary=Item_Category.__table__,
back_populates='categories', back_populates="categories",
) )
def __init__( def __init__(
@ -31,4 +33,4 @@ class Category(Base, UidMixin, UniqueNameMixin):
description: str | None = None, description: str | None = None,
): ):
self.name = name self.name = name
self.description = description self.description = description

@ -12,11 +12,12 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
class DeadlineDaemonLastRunDatetime(Base): class DeadlineDaemonLastRunDatetime(Base):
__table_args__ = ( __table_args__ = (
CheckConstraint( CheckConstraint(
'uid = true', "uid = true",
name = 'single_row_only', name="single_row_only",
), ),
) )
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True) uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
@ -24,4 +25,4 @@ class DeadlineDaemonLastRunDatetime(Base):
def __init__(self, time: datetime | None = None): def __init__(self, time: datetime | None = None):
if time is not None: if time is not None:
self.time = time self.time = time

@ -11,6 +11,7 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import UidMixin, UniqueNameMixin from .mixins import UidMixin, UniqueNameMixin
class Language(Base, UidMixin, UniqueNameMixin): class Language(Base, UidMixin, UniqueNameMixin):
iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True) iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True)

@ -10,13 +10,15 @@ from sqlalchemy.orm import (
from .Base import Base from .Base import Base
from .mixins import UidMixin, UniqueNameMixin from .mixins import UidMixin, UniqueNameMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from .BookcaseItem import BookcaseItem from .BookcaseItem import BookcaseItem
class MediaType(Base, UidMixin, UniqueNameMixin): class MediaType(Base, UidMixin, UniqueNameMixin):
description: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
items: Mapped[set[BookcaseItem]] = relationship(back_populates='media_type') items: Mapped[set[BookcaseItem]] = relationship(back_populates="media_type")
def __init__( def __init__(
self, self,
@ -25,5 +27,3 @@ class MediaType(Base, UidMixin, UniqueNameMixin):
): ):
self.name = name self.name = name
self.description = description self.description = description

@ -8,4 +8,18 @@ from .BookcaseShelf import BookcaseShelf
from .Category import Category from .Category import Category
from .DeadlineDaemonLastRunDatetime import DeadlineDaemonLastRunDatetime from .DeadlineDaemonLastRunDatetime import DeadlineDaemonLastRunDatetime
from .Language import Language from .Language import Language
from .MediaType import MediaType from .MediaType import MediaType
__all__ = [
"Author",
"Base",
"Bookcase",
"BookcaseItem",
"BookcaseItemBorrowing",
"BookcaseItemBorrowingQueue",
"BookcaseShelf",
"Category",
"DeadlineDaemonLastRunDatetime",
"Language",
"MediaType",
]

@ -1,5 +1,4 @@
from alembic import context from alembic import context
from flask import current_app
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
@ -14,7 +13,8 @@ if config.config_file_name is not None:
Config.load_configuration({}) Config.load_configuration({})
config.set_main_option('sqlalchemy.url', Config.db_string()) config.set_main_option("sqlalchemy.url", Config.db_string())
# This will make sure alembic doesn't generate empty migrations # This will make sure alembic doesn't generate empty migrations
# https://stackoverflow.com/questions/70203927/how-to-prevent-alembic-revision-autogenerate-from-making-revision-file-if-it-h # https://stackoverflow.com/questions/70203927/how-to-prevent-alembic-revision-autogenerate-from-making-revision-file-if-it-h
@ -23,7 +23,8 @@ def _process_revision_directives(context, revision, directives):
script = directives[0] script = directives[0]
if script.upgrade_ops.is_empty(): if script.upgrade_ops.is_empty():
directives[:] = [] directives[:] = []
print('No changes in schema detected. Not generating migration.') print("No changes in schema detected. Not generating migration.")
def run_migrations_online() -> None: def run_migrations_online() -> None:
connectable = engine_from_config( connectable = engine_from_config(
@ -36,11 +37,9 @@ def run_migrations_online() -> None:
context.configure( context.configure(
connection=connection, connection=connection,
target_metadata=Base.metadata, target_metadata=Base.metadata,
# Extended type checking with alembic when generating migrations # Extended type checking with alembic when generating migrations
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect # https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
compare_type=True, compare_type=True,
# This is required for ALTER TABLE to work with sqlite. # This is required for ALTER TABLE to work with sqlite.
# It should have no effect on postgreSQL # It should have no effect on postgreSQL
# https://alembic.sqlalchemy.org/en/latest/batch.html # https://alembic.sqlalchemy.org/en/latest/batch.html
@ -51,6 +50,7 @@ def run_migrations_online() -> None:
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
# We don't have any good reasons to generate raw sql migrations, # We don't have any good reasons to generate raw sql migrations,
# so the `run_migrations_offline` has been removed # so the `run_migrations_offline` has been removed
run_migrations_online() run_migrations_online()

@ -1,16 +1,17 @@
"""initial_migration """initial_migration
Revision ID: 7dfbf8a8dec8 Revision ID: 7dfbf8a8dec8
Revises: Revises:
Create Date: 2024-07-31 21:07:13.434012 Create Date: 2024-07-31 21:07:13.434012
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '7dfbf8a8dec8' revision = "7dfbf8a8dec8"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,166 +19,243 @@ depends_on = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('Author', op.create_table(
sa.Column('uid', sa.Integer(), nullable=False), "Author",
sa.Column('name', sa.Text(), nullable=False), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Author')) sa.Column("name", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Author")),
) )
with op.batch_alter_table('Author', schema=None) as batch_op: with op.batch_alter_table("Author", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_Author_name'), ['name'], unique=True) batch_op.create_index(batch_op.f("ix_Author_name"), ["name"], unique=True)
op.create_table('Bookcase', op.create_table(
sa.Column('description', sa.Text(), nullable=True), "Bookcase",
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("description", sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=False), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Bookcase')) sa.Column("name", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Bookcase")),
) )
with op.batch_alter_table('Bookcase', schema=None) as batch_op: with op.batch_alter_table("Bookcase", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_Bookcase_name'), ['name'], unique=True) batch_op.create_index(batch_op.f("ix_Bookcase_name"), ["name"], unique=True)
op.create_table('Category', op.create_table(
sa.Column('description', sa.Text(), nullable=True), "Category",
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("description", sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=False), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Category')) sa.Column("name", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Category")),
) )
with op.batch_alter_table('Category', schema=None) as batch_op: with op.batch_alter_table("Category", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_Category_name'), ['name'], unique=True) batch_op.create_index(batch_op.f("ix_Category_name"), ["name"], unique=True)
op.create_table('DeadlineDaemonLastRunDatetime', op.create_table(
sa.Column('uid', sa.Boolean(), nullable=False), "DeadlineDaemonLastRunDatetime",
sa.Column('time', sa.DateTime(), nullable=False), sa.Column("uid", sa.Boolean(), nullable=False),
sa.CheckConstraint('uid = true', name=op.f('ck_DeadlineDaemonLastRunDatetime_`single_row_only`')), sa.Column("time", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_DeadlineDaemonLastRunDatetime')) sa.CheckConstraint(
"uid = true",
name=op.f("ck_DeadlineDaemonLastRunDatetime_`single_row_only`"),
),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_DeadlineDaemonLastRunDatetime")),
) )
op.create_table('Language', op.create_table(
sa.Column('iso639_1_code', sa.String(length=2), nullable=False), "Language",
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("iso639_1_code", sa.String(length=2), nullable=False),
sa.Column('name', sa.Text(), nullable=False), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Language')) sa.Column("name", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Language")),
) )
with op.batch_alter_table('Language', schema=None) as batch_op: with op.batch_alter_table("Language", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_Language_iso639_1_code'), ['iso639_1_code'], unique=True) batch_op.create_index(
batch_op.create_index(batch_op.f('ix_Language_name'), ['name'], unique=True) batch_op.f("ix_Language_iso639_1_code"), ["iso639_1_code"], unique=True
)
batch_op.create_index(batch_op.f("ix_Language_name"), ["name"], unique=True)
op.create_table('MediaType', op.create_table(
sa.Column('description', sa.Text(), nullable=True), "MediaType",
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("description", sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=False), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_MediaType')) sa.Column("name", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_MediaType")),
) )
with op.batch_alter_table('MediaType', schema=None) as batch_op: with op.batch_alter_table("MediaType", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_MediaType_name'), ['name'], unique=True) batch_op.create_index(batch_op.f("ix_MediaType_name"), ["name"], unique=True)
op.create_table('BookcaseShelf', op.create_table(
sa.Column('description', sa.Text(), nullable=True), "BookcaseShelf",
sa.Column('row', sa.SmallInteger(), nullable=False), sa.Column("description", sa.Text(), nullable=True),
sa.Column('column', sa.SmallInteger(), nullable=False), sa.Column("row", sa.SmallInteger(), nullable=False),
sa.Column('fk_bookcase_uid', sa.Integer(), nullable=False), sa.Column("column", sa.SmallInteger(), nullable=False),
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("fk_bookcase_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_bookcase_uid'], ['Bookcase.uid'], name=op.f('fk_BookcaseShelf_fk_bookcase_uid_Bookcase')), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseShelf')), sa.ForeignKeyConstraint(
sa.UniqueConstraint('column', 'fk_bookcase_uid', 'row', name=op.f('uq_BookcaseShelf_column')) ["fk_bookcase_uid"],
["Bookcase.uid"],
name=op.f("fk_BookcaseShelf_fk_bookcase_uid_Bookcase"),
),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseShelf")),
sa.UniqueConstraint(
"column", "fk_bookcase_uid", "row", name=op.f("uq_BookcaseShelf_column")
),
) )
op.create_table('BookcaseItem', op.create_table(
sa.Column('isbn', sa.String(), nullable=False), "BookcaseItem",
sa.Column('name', sa.Text(), nullable=False), sa.Column("isbn", sa.String(), nullable=False),
sa.Column('owner', sa.String(), nullable=False), sa.Column("name", sa.Text(), nullable=False),
sa.Column('amount', sa.SmallInteger(), nullable=False), sa.Column("owner", sa.String(), nullable=False),
sa.Column('fk_media_type_uid', sa.Integer(), nullable=False), sa.Column("amount", sa.SmallInteger(), nullable=False),
sa.Column('fk_bookcase_shelf_uid', sa.Integer(), nullable=False), sa.Column("fk_media_type_uid", sa.Integer(), nullable=False),
sa.Column('fk_language_uid', sa.Integer(), nullable=True), sa.Column("fk_bookcase_shelf_uid", sa.Integer(), nullable=False),
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("fk_language_uid", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['fk_bookcase_shelf_uid'], ['BookcaseShelf.uid'], name=op.f('fk_BookcaseItem_fk_bookcase_shelf_uid_BookcaseShelf')), sa.Column("uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_language_uid'], ['Language.uid'], name=op.f('fk_BookcaseItem_fk_language_uid_Language')), sa.ForeignKeyConstraint(
sa.ForeignKeyConstraint(['fk_media_type_uid'], ['MediaType.uid'], name=op.f('fk_BookcaseItem_fk_media_type_uid_MediaType')), ["fk_bookcase_shelf_uid"],
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItem')) ["BookcaseShelf.uid"],
name=op.f("fk_BookcaseItem_fk_bookcase_shelf_uid_BookcaseShelf"),
),
sa.ForeignKeyConstraint(
["fk_language_uid"],
["Language.uid"],
name=op.f("fk_BookcaseItem_fk_language_uid_Language"),
),
sa.ForeignKeyConstraint(
["fk_media_type_uid"],
["MediaType.uid"],
name=op.f("fk_BookcaseItem_fk_media_type_uid_MediaType"),
),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItem")),
) )
with op.batch_alter_table('BookcaseItem', schema=None) as batch_op: with op.batch_alter_table("BookcaseItem", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_BookcaseItem_isbn'), ['isbn'], unique=True) batch_op.create_index(batch_op.f("ix_BookcaseItem_isbn"), ["isbn"], unique=True)
batch_op.create_index(batch_op.f('ix_BookcaseItem_name'), ['name'], unique=False) batch_op.create_index(
batch_op.f("ix_BookcaseItem_name"), ["name"], unique=False
)
op.create_table('BookcaseItemBorrowing', op.create_table(
sa.Column('username', sa.String(), nullable=False), "BookcaseItemBorrowing",
sa.Column('start_time', sa.DateTime(), nullable=False), sa.Column("username", sa.String(), nullable=False),
sa.Column('end_time', sa.DateTime(), nullable=False), sa.Column("start_time", sa.DateTime(), nullable=False),
sa.Column('delivered', sa.DateTime(), nullable=True), sa.Column("end_time", sa.DateTime(), nullable=False),
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False), sa.Column("delivered", sa.DateTime(), nullable=True),
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("fk_bookcase_item_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowing_fk_bookcase_item_uid_BookcaseItem')), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItemBorrowing')) sa.ForeignKeyConstraint(
["fk_bookcase_item_uid"],
["BookcaseItem.uid"],
name=op.f("fk_BookcaseItemBorrowing_fk_bookcase_item_uid_BookcaseItem"),
),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItemBorrowing")),
) )
with op.batch_alter_table('BookcaseItemBorrowing', schema=None) as batch_op: with op.batch_alter_table("BookcaseItemBorrowing", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_BookcaseItemBorrowing_fk_bookcase_item_uid'), ['fk_bookcase_item_uid'], unique=False) batch_op.create_index(
batch_op.f("ix_BookcaseItemBorrowing_fk_bookcase_item_uid"),
["fk_bookcase_item_uid"],
unique=False,
)
op.create_table('BookcaseItemBorrowingQueue', op.create_table(
sa.Column('username', sa.String(), nullable=False), "BookcaseItemBorrowingQueue",
sa.Column('entered_queue_time', sa.DateTime(), nullable=False), sa.Column("username", sa.String(), nullable=False),
sa.Column('item_became_available_time', sa.DateTime(), nullable=True), sa.Column("entered_queue_time", sa.DateTime(), nullable=False),
sa.Column('expired', sa.Boolean(), nullable=True), sa.Column("item_became_available_time", sa.DateTime(), nullable=True),
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False), sa.Column("expired", sa.Boolean(), nullable=True),
sa.Column('uid', sa.Integer(), nullable=False), sa.Column("fk_bookcase_item_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowingQueue_fk_bookcase_item_uid_BookcaseItem')), sa.Column("uid", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItemBorrowingQueue')) sa.ForeignKeyConstraint(
["fk_bookcase_item_uid"],
["BookcaseItem.uid"],
name=op.f(
"fk_BookcaseItemBorrowingQueue_fk_bookcase_item_uid_BookcaseItem"
),
),
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItemBorrowingQueue")),
) )
with op.batch_alter_table('BookcaseItemBorrowingQueue', schema=None) as batch_op: with op.batch_alter_table("BookcaseItemBorrowingQueue", schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid'), ['fk_bookcase_item_uid'], unique=False) batch_op.create_index(
batch_op.f("ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid"),
["fk_bookcase_item_uid"],
unique=False,
)
op.create_table('Item_Author', op.create_table(
sa.Column('fk_item_uid', sa.Integer(), nullable=False), "Item_Author",
sa.Column('fk_author_uid', sa.Integer(), nullable=False), sa.Column("fk_item_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_author_uid'], ['Author.uid'], name=op.f('fk_Item_Author_fk_author_uid_Author')), sa.Column("fk_author_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_Item_Author_fk_item_uid_BookcaseItem')), sa.ForeignKeyConstraint(
sa.PrimaryKeyConstraint('fk_item_uid', 'fk_author_uid', name=op.f('pk_Item_Author')) ["fk_author_uid"],
["Author.uid"],
name=op.f("fk_Item_Author_fk_author_uid_Author"),
),
sa.ForeignKeyConstraint(
["fk_item_uid"],
["BookcaseItem.uid"],
name=op.f("fk_Item_Author_fk_item_uid_BookcaseItem"),
),
sa.PrimaryKeyConstraint(
"fk_item_uid", "fk_author_uid", name=op.f("pk_Item_Author")
),
) )
op.create_table('Item_Category', op.create_table(
sa.Column('fk_item_uid', sa.Integer(), nullable=False), "Item_Category",
sa.Column('fk_category_uid', sa.Integer(), nullable=False), sa.Column("fk_item_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_category_uid'], ['Category.uid'], name=op.f('fk_Item_Category_fk_category_uid_Category')), sa.Column("fk_category_uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['fk_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_Item_Category_fk_item_uid_BookcaseItem')), sa.ForeignKeyConstraint(
sa.PrimaryKeyConstraint('fk_item_uid', 'fk_category_uid', name=op.f('pk_Item_Category')) ["fk_category_uid"],
["Category.uid"],
name=op.f("fk_Item_Category_fk_category_uid_Category"),
),
sa.ForeignKeyConstraint(
["fk_item_uid"],
["BookcaseItem.uid"],
name=op.f("fk_Item_Category_fk_item_uid_BookcaseItem"),
),
sa.PrimaryKeyConstraint(
"fk_item_uid", "fk_category_uid", name=op.f("pk_Item_Category")
),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('Item_Category') op.drop_table("Item_Category")
op.drop_table('Item_Author') op.drop_table("Item_Author")
with op.batch_alter_table('BookcaseItemBorrowingQueue', schema=None) as batch_op: with op.batch_alter_table("BookcaseItemBorrowingQueue", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid')) batch_op.drop_index(
batch_op.f("ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid")
)
op.drop_table('BookcaseItemBorrowingQueue') op.drop_table("BookcaseItemBorrowingQueue")
with op.batch_alter_table('BookcaseItemBorrowing', schema=None) as batch_op: with op.batch_alter_table("BookcaseItemBorrowing", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_BookcaseItemBorrowing_fk_bookcase_item_uid')) batch_op.drop_index(batch_op.f("ix_BookcaseItemBorrowing_fk_bookcase_item_uid"))
op.drop_table('BookcaseItemBorrowing') op.drop_table("BookcaseItemBorrowing")
with op.batch_alter_table('BookcaseItem', schema=None) as batch_op: with op.batch_alter_table("BookcaseItem", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_BookcaseItem_name')) batch_op.drop_index(batch_op.f("ix_BookcaseItem_name"))
batch_op.drop_index(batch_op.f('ix_BookcaseItem_isbn')) batch_op.drop_index(batch_op.f("ix_BookcaseItem_isbn"))
op.drop_table('BookcaseItem') op.drop_table("BookcaseItem")
op.drop_table('BookcaseShelf') op.drop_table("BookcaseShelf")
with op.batch_alter_table('MediaType', schema=None) as batch_op: with op.batch_alter_table("MediaType", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_MediaType_name')) batch_op.drop_index(batch_op.f("ix_MediaType_name"))
op.drop_table('MediaType') op.drop_table("MediaType")
with op.batch_alter_table('Language', schema=None) as batch_op: with op.batch_alter_table("Language", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_Language_name')) batch_op.drop_index(batch_op.f("ix_Language_name"))
batch_op.drop_index(batch_op.f('ix_Language_iso639_1_code')) batch_op.drop_index(batch_op.f("ix_Language_iso639_1_code"))
op.drop_table('Language') op.drop_table("Language")
op.drop_table('DeadlineDaemonLastRunDatetime') op.drop_table("DeadlineDaemonLastRunDatetime")
with op.batch_alter_table('Category', schema=None) as batch_op: with op.batch_alter_table("Category", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_Category_name')) batch_op.drop_index(batch_op.f("ix_Category_name"))
op.drop_table('Category') op.drop_table("Category")
with op.batch_alter_table('Bookcase', schema=None) as batch_op: with op.batch_alter_table("Bookcase", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_Bookcase_name')) batch_op.drop_index(batch_op.f("ix_Bookcase_name"))
op.drop_table('Bookcase') op.drop_table("Bookcase")
with op.batch_alter_table('Author', schema=None) as batch_op: with op.batch_alter_table("Author", schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_Author_name')) batch_op.drop_index(batch_op.f("ix_Author_name"))
op.drop_table('Author') op.drop_table("Author")
# ### end Alembic commands ### # ### end Alembic commands ###

@ -9,6 +9,7 @@ from sqlalchemy.orm import (
from worblehat.flaskapp.database import db from worblehat.flaskapp.database import db
class UidMixin(object): class UidMixin(object):
uid: Mapped[int] = mapped_column(Integer, primary_key=True) uid: Mapped[int] = mapped_column(Integer, primary_key=True)
@ -28,4 +29,4 @@ class UidMixin(object):
This method defaults to using the flask_sqlalchemy session. This method defaults to using the flask_sqlalchemy session.
It will not work outside of a request context, unless another session is provided. It will not work outside of a request context, unless another session is provided.
""" """
return sql_session.query(cls).where(cls.uid == uid).one_or_404() return sql_session.query(cls).where(cls.uid == uid).one_or_404()

@ -9,6 +9,7 @@ from sqlalchemy.orm import (
from worblehat.flaskapp.database import db from worblehat.flaskapp.database import db
class UniqueNameMixin(object): class UniqueNameMixin(object):
name: Mapped[str] = mapped_column(Text, unique=True, index=True) name: Mapped[str] = mapped_column(Text, unique=True, index=True)
@ -28,4 +29,4 @@ class UniqueNameMixin(object):
This method defaults to using the flask_sqlalchemy session. This method defaults to using the flask_sqlalchemy session.
It will not work outside of a request context, unless another session is provided. It will not work outside of a request context, unless another session is provided.
""" """
return sql_session.query(cls).where(cls.name == name).one_or_404() return sql_session.query(cls).where(cls.name == name).one_or_404()

@ -1,6 +1,7 @@
from sqlalchemy.orm import declared_attr from sqlalchemy.orm import declared_attr
class XrefMixin(object): class XrefMixin(object):
@declared_attr.directive @declared_attr.directive
def __tablename__(cls) -> str: def __tablename__(cls) -> str:
return f'xref_{cls.__name__.lower()}' return f"xref_{cls.__name__.lower()}"

@ -1,2 +1,4 @@
from .UidMixin import UidMixin from .UidMixin import UidMixin
from .UniqueNameMixin import UniqueNameMixin from .UniqueNameMixin import UniqueNameMixin
__all__ = ["UidMixin", "UniqueNameMixin"]

@ -1,5 +1,4 @@
from sqlalchemy import ( from sqlalchemy import (
Integer,
ForeignKey, ForeignKey,
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import (
@ -10,6 +9,11 @@ from sqlalchemy.orm import (
from ..Base import Base from ..Base import Base
from ..mixins.XrefMixin import XrefMixin from ..mixins.XrefMixin import XrefMixin
class Item_Author(Base, XrefMixin): class Item_Author(Base, XrefMixin):
fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True) fk_item_uid: Mapped[int] = mapped_column(
fk_author_uid: Mapped[int] = mapped_column(ForeignKey('Author.uid'), primary_key=True) ForeignKey("BookcaseItem.uid"), primary_key=True
)
fk_author_uid: Mapped[int] = mapped_column(
ForeignKey("Author.uid"), primary_key=True
)

@ -1,5 +1,4 @@
from sqlalchemy import ( from sqlalchemy import (
Integer,
ForeignKey, ForeignKey,
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import (
@ -10,6 +9,11 @@ from sqlalchemy.orm import (
from ..Base import Base from ..Base import Base
from ..mixins.XrefMixin import XrefMixin from ..mixins.XrefMixin import XrefMixin
class Item_Category(Base, XrefMixin): class Item_Category(Base, XrefMixin):
fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True) fk_item_uid: Mapped[int] = mapped_column(
fk_category_uid: Mapped[int] = mapped_column(ForeignKey('Category.uid'), primary_key=True) ForeignKey("BookcaseItem.uid"), primary_key=True
)
fk_category_uid: Mapped[int] = mapped_column(
ForeignKey("Category.uid"), primary_key=True
)

@ -1,2 +1,7 @@
from .Item_Author import Item_Author from .Item_Author import Item_Author
from .Item_Category import Item_Category from .Item_Category import Item_Category
__all__ = [
"Item_Author",
"Item_Category",
]

@ -1,6 +1,6 @@
from .argument_parser import ( from .argument_parser import (
arg_parser, arg_parser,
devscripts_arg_parser, devscripts_arg_parser,
) )
from .bookcase_item import ( from .bookcase_item import (
create_bookcase_item_from_isbn, create_bookcase_item_from_isbn,
@ -8,4 +8,14 @@ from .bookcase_item import (
) )
from .config import Config from .config import Config
from .email import send_email from .email import send_email
from .seed_test_data import seed_data from .seed_test_data import seed_data
__all__ = [
"arg_parser",
"devscripts_arg_parser",
"Config",
"create_bookcase_item_from_isbn",
"is_valid_isbn",
"send_email",
"seed_data",
]

@ -1,66 +1,69 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from pathlib import Path from pathlib import Path
def _is_valid_file(parser: ArgumentParser, arg: str) -> Path: def _is_valid_file(parser: ArgumentParser, arg: str) -> Path:
path = Path(arg) path = Path(arg)
if not path.is_file(): if not path.is_file():
parser.error(f'The file {arg} does not exist!') parser.error(f"The file {arg} does not exist!")
return path return path
arg_parser = ArgumentParser( arg_parser = ArgumentParser(
description = 'Worblehat library management system', description="Worblehat library management system",
) )
subparsers = arg_parser.add_subparsers(dest='command') subparsers = arg_parser.add_subparsers(dest="command")
subparsers.add_parser( subparsers.add_parser(
'deadline-daemon', "deadline-daemon",
help = 'Initialize a single pass of the daemon which sends deadline emails', help="Initialize a single pass of the daemon which sends deadline emails",
) )
subparsers.add_parser( subparsers.add_parser(
'cli', "cli",
help = 'Start the command line interface', help="Start the command line interface",
) )
subparsers.add_parser( subparsers.add_parser(
'flask-dev', "flask-dev",
help = 'Start the web interface in development mode', help="Start the web interface in development mode",
) )
subparsers.add_parser( subparsers.add_parser(
'flask-prod', "flask-prod",
help = 'Start the web interface in production mode', help="Start the web interface in production mode",
) )
devscripts_arg_parser = subparsers.add_parser('devscripts', help='Run development scripts') devscripts_arg_parser = subparsers.add_parser(
devscripts_subparsers = devscripts_arg_parser.add_subparsers(dest='script') "devscripts", help="Run development scripts"
)
devscripts_subparsers = devscripts_arg_parser.add_subparsers(dest="script")
devscripts_subparsers.add_parser( devscripts_subparsers.add_parser(
'seed-test-data', "seed-test-data",
help = 'Seed test data in the database', help="Seed test data in the database",
) )
devscripts_subparsers.add_parser( devscripts_subparsers.add_parser(
'seed-content-for-deadline-daemon', "seed-content-for-deadline-daemon",
help = 'Seed data tailorded for testing the deadline daemon, into the database', help="Seed data tailorded for testing the deadline daemon, into the database",
) )
arg_parser.add_argument( arg_parser.add_argument(
'-V', "-V",
'--version', "--version",
action = 'store_true', action="store_true",
help = 'Print version and exit', help="Print version and exit",
) )
arg_parser.add_argument( arg_parser.add_argument(
'-c', "-c",
'--config', "--config",
type=lambda x: _is_valid_file(arg_parser, x), type=lambda x: _is_valid_file(arg_parser, x),
help = 'Path to config file', help="Path to config file",
dest = 'config_file', dest="config_file",
metavar = 'FILE', metavar="FILE",
) )
arg_parser.add_argument( arg_parser.add_argument(
'-p', "-p",
'--print-config', "--print-config",
action = 'store_true', action="store_true",
help = 'Print configuration and quit', help="Print configuration and quit",
) )

@ -10,22 +10,27 @@ from ..models import (
Language, Language,
) )
def is_valid_pvv_isbn(isbn: str) -> bool: def is_valid_pvv_isbn(isbn: str) -> bool:
try: try:
int(isbn) int(isbn)
except ValueError: except ValueError:
return False return False
return len(isbn) == 8 return len(isbn) == 8
def is_valid_isbn(isbn: str) -> bool: def is_valid_isbn(isbn: str) -> bool:
return any([ return any(
isbnlib.is_isbn10(isbn), [
isbnlib.is_isbn13(isbn), isbnlib.is_isbn10(isbn),
]) isbnlib.is_isbn13(isbn),
]
)
def create_bookcase_item_from_isbn(isbn: str, sql_session: Session) -> BookcaseItem | None: def create_bookcase_item_from_isbn(
isbn: str, sql_session: Session
) -> BookcaseItem | None:
""" """
This function fetches metadata for the given ISBN and creates a BookcaseItem from it. This function fetches metadata for the given ISBN and creates a BookcaseItem from it.
It does so using a database connection to connect it to the correct authors and language It does so using a database connection to connect it to the correct authors and language
@ -43,18 +48,17 @@ def create_bookcase_item_from_isbn(isbn: str, sql_session: Session) -> BookcaseI
metadata = metadata[0] metadata = metadata[0]
bookcase_item = BookcaseItem( bookcase_item = BookcaseItem(
name = metadata.title, name=metadata.title,
isbn = int(isbn.replace('-', '')), isbn=int(isbn.replace("-", "")),
) )
if len(authors := metadata.authors) > 0: if len(authors := metadata.authors) > 0:
for author in authors: for author in authors:
bookcase_item.authors.add(Author(author)) bookcase_item.authors.add(Author(author))
if (language := metadata.language): if language := metadata.language:
bookcase_item.language = sql_session.scalars( bookcase_item.language = sql_session.scalars(
select(Language) select(Language).where(Language.iso639_1_code == language)
.where(Language.iso639_1_code == language)
).one() ).one()
return bookcase_item return bookcase_item

@ -18,30 +18,31 @@ class Config:
_config = None _config = None
_expected_config_file_locations = [ _expected_config_file_locations = [
Path('./config.toml'), Path("./config.toml"),
Path('~/.config/worblehat/config.toml'), Path("~/.config/worblehat/config.toml"),
Path('/var/lib/worblehat/config.toml'), Path("/var/lib/worblehat/config.toml"),
] ]
def __class_getitem__(cls, name: str) -> Any: def __class_getitem__(cls, name: str) -> Any:
if cls._config is None: if cls._config is None:
raise RuntimeError('Configuration not loaded, call Config.load_configuration() first.') raise RuntimeError(
"Configuration not loaded, call Config.load_configuration() first."
)
__config = cls._config __config = cls._config
for attr in name.split('.'): for attr in name.split("."):
__config = __config.get(attr) __config = __config.get(attr)
if __config is None: if __config is None:
raise AttributeError(f'No such attribute: {name}') raise AttributeError(f"No such attribute: {name}")
return __config return __config
@staticmethod @staticmethod
def read_password(password_field: str) -> str: def read_password(password_field: str) -> str:
if Path(password_field).is_file(): if Path(password_field).is_file():
with open(password_field, 'r') as f: with open(password_field, "r") as f:
return f.read() return f.read()
else: else:
return password_field return password_field
@classmethod @classmethod
def _locate_configuration_file(cls) -> Path | None: def _locate_configuration_file(cls) -> Path | None:
@ -49,48 +50,46 @@ class Config:
if path.is_file(): if path.is_file():
return path return path
@classmethod @classmethod
def _load_configuration_from_file(cls, config_file_path: str | None) -> dict[str, any]: def _load_configuration_from_file(
cls, config_file_path: str | None
) -> dict[str, any]:
if config_file_path is None: if config_file_path is None:
config_file_path = cls._locate_configuration_file() config_file_path = cls._locate_configuration_file()
if config_file_path is None: if config_file_path is None:
print('Error: could not locate configuration file.') print("Error: could not locate configuration file.")
exit(1) exit(1)
with open(config_file_path, 'rb') as config_file: with open(config_file_path, "rb") as config_file:
args = tomllib.load(config_file) args = tomllib.load(config_file)
return args return args
@classmethod @classmethod
def db_string(cls) -> str: def db_string(cls) -> str:
db_type = cls._config.get('database').get('type') db_type = cls._config.get("database").get("type")
if db_type == 'sqlite': if db_type == "sqlite":
path = Path(cls._config.get('database').get('sqlite').get('path')) path = Path(cls._config.get("database").get("sqlite").get("path"))
return f"sqlite:///{path.absolute()}" return f"sqlite:///{path.absolute()}"
elif db_type == 'postgresql': elif db_type == "postgresql":
db_config = cls._config.get('database').get('postgresql') db_config = cls._config.get("database").get("postgresql")
hostname = db_config.get('hostname') hostname = db_config.get("hostname")
port = db_config.get('port') port = db_config.get("port")
username = db_config.get('username') username = db_config.get("username")
password = cls.read_password(db_config.get('password')) password = cls.read_password(db_config.get("password"))
database = db_config.get('database') database = db_config.get("database")
return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}" return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}"
else: else:
print(f"Error: unknown database type '{db_config.get('type')}'") print(f"Error: unknown database type '{db_config.get('type')}'")
exit(1) exit(1)
@classmethod @classmethod
def debug(cls) -> str: def debug(cls) -> str:
return pformat(cls._config) return pformat(cls._config)
@classmethod @classmethod
def load_configuration(cls, args: dict[str, any]) -> dict[str, any]: def load_configuration(cls, args: dict[str, any]) -> dict[str, any]:
cls._config = cls._load_configuration_from_file(args.get('config_file')) cls._config = cls._load_configuration_from_file(args.get("config_file"))

@ -10,26 +10,26 @@ from .config import Config
def send_email(to: str, subject: str, body: str): def send_email(to: str, subject: str, body: str):
msg = MIMEMultipart() msg = MIMEMultipart()
msg['From'] = Config['smtp.from'] msg["From"] = Config["smtp.from"]
msg['To'] = to msg["To"] = to
if Config['smtp.subject_prefix']: if Config["smtp.subject_prefix"]:
msg['Subject'] = f"{Config['smtp.subject_prefix']} {subject}" msg["Subject"] = f"{Config['smtp.subject_prefix']} {subject}"
else: else:
msg['Subject'] = subject msg["Subject"] = subject
msg.attach(MIMEText(body, 'plain')) msg.attach(MIMEText(body, "plain"))
if Config['smtp.enabled'] and not Config['deadline_daemon.dryrun']: if Config["smtp.enabled"] and not Config["deadline_daemon.dryrun"]:
try: try:
with smtplib.SMTP(Config['smtp.host'], Config['smtp.port']) as server: with smtplib.SMTP(Config["smtp.host"], Config["smtp.port"]) as server:
server.starttls() server.starttls()
server.login( server.login(
Config['smtp.username'], Config["smtp.username"],
Config.read_password(Config['smtp.password']), Config.read_password(Config["smtp.password"]),
) )
server.sendmail(Config['smtp.from'], to, msg.as_string()) server.sendmail(Config["smtp.from"], to, msg.as_string())
except Exception as err: except Exception as err:
print('Error: could not send email.') print("Error: could not send email.")
print(err) print(err)
else: else:
print('Debug: Email sending is disabled, so the following email was not sent:') print("Debug: Email sending is disabled, so the following email was not sent:")
print(indent(msg.as_string(), ' ')) print(indent(msg.as_string(), " "))

@ -3,26 +3,29 @@ from typing import Set
# TODO: Add more languages # TODO: Add more languages
LANGUAGES: set[str] = set([ LANGUAGES: set[str] = set(
"no", [
"en", "no",
"de", "en",
"fr", "de",
"es", "fr",
"it", "es",
"sv", "it",
"da", "sv",
"fi", "da",
"ru", "fi",
"zh", "ru",
"ja", "zh",
"ko", "ja",
]) "ko",
]
)
@dataclass @dataclass
class BookMetadata: class BookMetadata:
"""A class representing metadata for a book.""" """A class representing metadata for a book."""
isbn: str isbn: str
title: str title: str
# The source of the metadata provider # The source of the metadata provider
@ -35,28 +38,30 @@ class BookMetadata:
def to_dict(self) -> dict[str, any]: def to_dict(self) -> dict[str, any]:
return { return {
'isbn': self.isbn, "isbn": self.isbn,
'title': self.title, "title": self.title,
'source': self.metadata_source_id(), "source": self.metadata_source_id(),
'authors': set() if self.authors is None else self.authors, "authors": set() if self.authors is None else self.authors,
'language': self.language, "language": self.language,
'publish_date': self.publish_date, "publish_date": self.publish_date,
'num_pages': self.num_pages, "num_pages": self.num_pages,
'subjects': set() if self.subjects is None else self.subjects "subjects": set() if self.subjects is None else self.subjects,
} }
def validate(self) -> None: def validate(self) -> None:
if not self.isbn: if not self.isbn:
raise ValueError('Missing ISBN') raise ValueError("Missing ISBN")
if not self.title: if not self.title:
raise ValueError('Missing title') raise ValueError("Missing title")
if not self.source: if not self.source:
raise ValueError('Missing source') raise ValueError("Missing source")
if not self.authors: if not self.authors:
raise ValueError('Missing authors') raise ValueError("Missing authors")
if self.language is not None and self.language not in LANGUAGES: if self.language is not None and self.language not in LANGUAGES:
raise ValueError(f'Invalid language: {self.language}. Consider adding it to the LANGUAGES set if you think this is a mistake.') raise ValueError(
f"Invalid language: {self.language}. Consider adding it to the LANGUAGES set if you think this is a mistake."
)
if self.num_pages is not None and self.num_pages < 0: if self.num_pages is not None and self.num_pages < 0:
raise ValueError(f'Invalid number of pages: {self.num_pages}') raise ValueError(f"Invalid number of pages: {self.num_pages}")

@ -1,7 +1,8 @@
#base fetcher. # base fetcher.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from .BookMetadata import BookMetadata from .BookMetadata import BookMetadata
class BookMetadataFetcher(ABC): class BookMetadataFetcher(ABC):
""" """
A base class for metadata fetchers. A base class for metadata fetchers.
@ -17,4 +18,4 @@ class BookMetadataFetcher(ABC):
@abstractmethod @abstractmethod
def fetch_metadata(cls, isbn: str) -> BookMetadata | None: def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
"""Tries to fetch metadata for the given ISBN.""" """Tries to fetch metadata for the given ISBN."""
pass pass

@ -11,14 +11,14 @@ from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
class GoogleBooksFetcher(BookMetadataFetcher): class GoogleBooksFetcher(BookMetadataFetcher):
@classmethod @classmethod
def metadata_source_id(_cls) -> str: def metadata_source_id(_cls) -> str:
return "google_books" return "google_books"
@classmethod @classmethod
def fetch_metadata(cls, isbn: str) -> BookMetadata | None: def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
try: try:
jsonInput = requests.get( jsonInput = requests.get(
f"https://www.googleapis.com/books/v1/volumes", "https://www.googleapis.com/books/v1/volumes",
params = {"q": f"isbn:{isbn}"}, params={"q": f"isbn:{isbn}"},
).json() ).json()
data = jsonInput.get("items")[0].get("volumeInfo") data = jsonInput.get("items")[0].get("volumeInfo")
@ -34,18 +34,18 @@ class GoogleBooksFetcher(BookMetadataFetcher):
return None return None
return BookMetadata( return BookMetadata(
isbn = isbn, isbn=isbn,
title = title, title=title,
source = cls.metadata_source_id(), source=cls.metadata_source_id(),
authors = authors, authors=authors,
language = languages, language=languages,
publish_date = publishDate, publish_date=publishDate,
num_pages = numberOfPages, num_pages=numberOfPages,
subjects = subjects, subjects=subjects,
) )
if __name__ == '__main__': if __name__ == "__main__":
book_data = GoogleBooksFetcher.fetch_metadata('0132624788') book_data = GoogleBooksFetcher.fetch_metadata("0132624788")
book_data.validate() book_data.validate()
print(book_data) print(book_data)

@ -15,7 +15,7 @@ LANGUAGE_MAP = {
class OpenLibraryFetcher(BookMetadataFetcher): class OpenLibraryFetcher(BookMetadataFetcher):
@classmethod @classmethod
def metadata_source_id(_cls) -> str: def metadata_source_id(_cls) -> str:
return "open_library" return "open_library"
@classmethod @classmethod
def fetch_metadata(cls, isbn: str) -> BookMetadata | None: def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
@ -25,8 +25,12 @@ class OpenLibraryFetcher(BookMetadataFetcher):
author_keys = jsonInput.get("authors") or [] author_keys = jsonInput.get("authors") or []
author_names = set() author_names = set()
for author_key in author_keys: for author_key in author_keys:
key = author_key.get('key') key = author_key.get("key")
author_name = requests.get(f"https://openlibrary.org/{key}.json").json().get("name") author_name = (
requests.get(f"https://openlibrary.org/{key}.json")
.json()
.get("name")
)
author_names.add(author_name) author_names.add(author_name)
title = jsonInput.get("title") title = jsonInput.get("title")
@ -37,25 +41,30 @@ class OpenLibraryFetcher(BookMetadataFetcher):
numberOfPages = int(numberOfPages) numberOfPages = int(numberOfPages)
language_key = jsonInput.get("languages")[0].get("key") language_key = jsonInput.get("languages")[0].get("key")
language = requests.get(f"https://openlibrary.org/{language_key}.json").json().get("identifiers").get("iso_639_1")[0] language = (
requests.get(f"https://openlibrary.org/{language_key}.json")
.json()
.get("identifiers")
.get("iso_639_1")[0]
)
subjects = set(jsonInput.get("subjects") or []) subjects = set(jsonInput.get("subjects") or [])
except Exception: except Exception:
return None return None
return BookMetadata( return BookMetadata(
isbn = isbn, isbn=isbn,
title = title, title=title,
source = cls.metadata_source_id(), source=cls.metadata_source_id(),
authors = author_names, authors=author_names,
language = language, language=language,
publish_date = publishDate, publish_date=publishDate,
num_pages = numberOfPages, num_pages=numberOfPages,
subjects = subjects, subjects=subjects,
) )
if __name__ == '__main__': if __name__ == "__main__":
book_data = OpenLibraryFetcher.fetch_metadata('9788205530751') book_data = OpenLibraryFetcher.fetch_metadata("9788205530751")
book_data.validate() book_data.validate()
print(book_data) print(book_data)

@ -30,7 +30,7 @@ LANGUAGE_MAP = {
class OutlandScraperFetcher(BookMetadataFetcher): class OutlandScraperFetcher(BookMetadataFetcher):
@classmethod @classmethod
def metadata_source_id(_cls) -> str: def metadata_source_id(_cls) -> str:
return "outland_scraper" return "outland_scraper"
@classmethod @classmethod
def fetch_metadata(cls, isbn: str) -> BookMetadata | None: def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
@ -50,7 +50,7 @@ class OutlandScraperFetcher(BookMetadataFetcher):
title = soup.find_all("span", class_="base")[0].text title = soup.find_all("span", class_="base")[0].text
releaseDate = soup.find_all("span", class_="release-date")[0].text.strip() releaseDate = soup.find_all("span", class_="release-date")[0].text.strip()
releaseDate = releaseDate[-4:] # only keep year releaseDate = releaseDate[-4:] # only keep year
bookData = { bookData = {
"Title": title, "Title": title,
@ -67,7 +67,7 @@ class OutlandScraperFetcher(BookMetadataFetcher):
"NumberOfPages": "Antall Sider", "NumberOfPages": "Antall Sider",
"Genre": "Sjanger", "Genre": "Sjanger",
"Language": "Språk", "Language": "Språk",
"Subjects": "Serie" "Subjects": "Serie",
} }
for value in data: for value in data:
@ -92,18 +92,18 @@ class OutlandScraperFetcher(BookMetadataFetcher):
return None return None
return BookMetadata( return BookMetadata(
isbn = isbn, isbn=isbn,
title = bookData.get('Title'), title=bookData.get("Title"),
source = cls.metadata_source_id(), source=cls.metadata_source_id(),
authors = bookData.get('Authors'), authors=bookData.get("Authors"),
language = bookData.get('Language'), language=bookData.get("Language"),
publish_date = bookData.get('PublishDate'), publish_date=bookData.get("PublishDate"),
num_pages = bookData.get('NumberOfPages'), num_pages=bookData.get("NumberOfPages"),
subjects = bookData.get('Subjects'), subjects=bookData.get("Subjects"),
) )
if __name__ == '__main__': if __name__ == "__main__":
book_data = OutlandScraperFetcher.fetch_metadata('9781947808225') book_data = OutlandScraperFetcher.fetch_metadata("9781947808225")
book_data.validate() book_data.validate()
print(book_data) print(book_data)

@ -1 +1,3 @@
from .book_metadata_fetcher import fetch_metadata_from_multiple_sources from .book_metadata_fetcher import fetch_metadata_from_multiple_sources
__all__ = ["fetch_metadata_from_multiple_sources"]

@ -10,7 +10,9 @@ from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadat
from worblehat.services.metadata_fetchers.GoogleBooksFetcher import GoogleBooksFetcher from worblehat.services.metadata_fetchers.GoogleBooksFetcher import GoogleBooksFetcher
from worblehat.services.metadata_fetchers.OpenLibraryFetcher import OpenLibraryFetcher from worblehat.services.metadata_fetchers.OpenLibraryFetcher import OpenLibraryFetcher
from worblehat.services.metadata_fetchers.OutlandScraperFetcher import OutlandScraperFetcher from worblehat.services.metadata_fetchers.OutlandScraperFetcher import (
OutlandScraperFetcher,
)
# The order of these fetchers determines the priority of the sources. # The order of these fetchers determines the priority of the sources.
@ -46,14 +48,16 @@ def fetch_metadata_from_multiple_sources(isbn: str, strict=False) -> list[BookMe
The results are always ordered in the same way as the fetchers are listed in the FETCHERS list. The results are always ordered in the same way as the fetchers are listed in the FETCHERS list.
""" """
isbn = isbn.replace('-', '').replace('_', '').strip().lower() isbn = isbn.replace("-", "").replace("_", "").strip().lower()
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric(): if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
raise ValueError('Invalid ISBN') raise ValueError("Invalid ISBN")
results: list[BookMetadata] = [] results: list[BookMetadata] = []
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [executor.submit(fetcher.fetch_metadata, isbn) for fetcher in FETCHERS] futures = [
executor.submit(fetcher.fetch_metadata, isbn) for fetcher in FETCHERS
]
for future in futures: for future in futures:
result = future.result() result = future.result()
@ -67,14 +71,15 @@ def fetch_metadata_from_multiple_sources(isbn: str, strict=False) -> list[BookMe
if strict: if strict:
raise e raise e
else: else:
print(f'Invalid metadata: {e}') print(f"Invalid metadata: {e}")
results.remove(result) results.remove(result)
return sort_metadata_by_priority(results) return sort_metadata_by_priority(results)
if __name__ == '__main__': if __name__ == "__main__":
from pprint import pprint from pprint import pprint
isbn = '0132624788'
isbn = "0132624788"
metadata = fetch_metadata_from_multiple_sources(isbn) metadata = fetch_metadata_from_multiple_sources(isbn)
pprint(metadata) pprint(metadata)

@ -17,20 +17,27 @@ from ..models import (
MediaType, MediaType,
) )
def seed_data(sql_session: Session = db.session): def seed_data(sql_session: Session = db.session):
media_types = [ media_types = [
MediaType(name='Book', description='A physical book'), MediaType(name="Book", description="A physical book"),
MediaType(name='Comic', description='A comic book'), MediaType(name="Comic", description="A comic book"),
MediaType(name='Video Game', description='A digital game for computers or games consoles'), MediaType(
MediaType(name='Tabletop Game', description='A physical game with cards, boards or similar') name="Video Game",
description="A digital game for computers or games consoles",
),
MediaType(
name="Tabletop Game",
description="A physical game with cards, boards or similar",
),
] ]
bookcases = [ bookcases = [
Bookcase(name='Unnamed A', description='White case across dibbler'), Bookcase(name="Unnamed A", description="White case across dibbler"),
Bookcase(name='Unnamed B', description='Math case in the working room'), Bookcase(name="Unnamed B", description="Math case in the working room"),
Bookcase(name='Unnamed C', description='Large case in the working room'), Bookcase(name="Unnamed C", description="Large case in the working room"),
Bookcase(name='Unnamed D', description='White comics case in the hallway'), Bookcase(name="Unnamed D", description="White comics case in the hallway"),
Bookcase(name='Unnamed E', description='Wooden comics case in the hallway'), Bookcase(name="Unnamed E", description="Wooden comics case in the hallway"),
] ]
shelfs = [ shelfs = [
@ -39,82 +46,110 @@ def seed_data(sql_session: Session = db.session):
BookcaseShelf(row=2, column=0, bookcase=bookcases[0]), BookcaseShelf(row=2, column=0, bookcase=bookcases[0]),
BookcaseShelf(row=3, column=0, bookcase=bookcases[0], description="Hacking"), BookcaseShelf(row=3, column=0, bookcase=bookcases[0], description="Hacking"),
BookcaseShelf(row=4, column=0, bookcase=bookcases[0], description="Hacking"), BookcaseShelf(row=4, column=0, bookcase=bookcases[0], description="Hacking"),
BookcaseShelf(row=0, column=1, bookcase=bookcases[0]), BookcaseShelf(row=0, column=1, bookcase=bookcases[0]),
BookcaseShelf(row=1, column=1, bookcase=bookcases[0]), BookcaseShelf(row=1, column=1, bookcase=bookcases[0]),
BookcaseShelf(row=2, column=1, bookcase=bookcases[0], description="DOS"), BookcaseShelf(row=2, column=1, bookcase=bookcases[0], description="DOS"),
BookcaseShelf(row=3, column=1, bookcase=bookcases[0], description="Food for thought"), BookcaseShelf(
row=3, column=1, bookcase=bookcases[0], description="Food for thought"
),
BookcaseShelf(row=4, column=1, bookcase=bookcases[0], description="CPP"), BookcaseShelf(row=4, column=1, bookcase=bookcases[0], description="CPP"),
BookcaseShelf(row=0, column=2, bookcase=bookcases[0]), BookcaseShelf(row=0, column=2, bookcase=bookcases[0]),
BookcaseShelf(row=1, column=2, bookcase=bookcases[0]), BookcaseShelf(row=1, column=2, bookcase=bookcases[0]),
BookcaseShelf(row=2, column=2, bookcase=bookcases[0], description="E = mc2"), BookcaseShelf(row=2, column=2, bookcase=bookcases[0], description="E = mc2"),
BookcaseShelf(row=3, column=2, bookcase=bookcases[0], description="OBJECTION!"), BookcaseShelf(row=3, column=2, bookcase=bookcases[0], description="OBJECTION!"),
BookcaseShelf(row=4, column=2, bookcase=bookcases[0], description="/home"), BookcaseShelf(row=4, column=2, bookcase=bookcases[0], description="/home"),
BookcaseShelf(row=0, column=3, bookcase=bookcases[0]), BookcaseShelf(row=0, column=3, bookcase=bookcases[0]),
BookcaseShelf(row=1, column=3, bookcase=bookcases[0], description="Big indonisian island"), BookcaseShelf(
row=1, column=3, bookcase=bookcases[0], description="Big indonisian island"
),
BookcaseShelf(row=2, column=3, bookcase=bookcases[0]), BookcaseShelf(row=2, column=3, bookcase=bookcases[0]),
BookcaseShelf(row=3, column=3, bookcase=bookcases[0], description="Div science"), BookcaseShelf(
row=3, column=3, bookcase=bookcases[0], description="Div science"
),
BookcaseShelf(row=4, column=3, bookcase=bookcases[0], description="/home"), BookcaseShelf(row=4, column=3, bookcase=bookcases[0], description="/home"),
BookcaseShelf(row=0, column=4, bookcase=bookcases[0]), BookcaseShelf(row=0, column=4, bookcase=bookcases[0]),
BookcaseShelf(row=1, column=4, bookcase=bookcases[0]), BookcaseShelf(row=1, column=4, bookcase=bookcases[0]),
BookcaseShelf(row=2, column=4, bookcase=bookcases[0], description="(not) computer vision"), BookcaseShelf(
BookcaseShelf(row=3, column=4, bookcase=bookcases[0], description="Low voltage"), row=2, column=4, bookcase=bookcases[0], description="(not) computer vision"
),
BookcaseShelf(
row=3, column=4, bookcase=bookcases[0], description="Low voltage"
),
BookcaseShelf(row=4, column=4, bookcase=bookcases[0], description="/home"), BookcaseShelf(row=4, column=4, bookcase=bookcases[0], description="/home"),
BookcaseShelf(row=0, column=5, bookcase=bookcases[0]), BookcaseShelf(row=0, column=5, bookcase=bookcases[0]),
BookcaseShelf(row=1, column=5, bookcase=bookcases[0]), BookcaseShelf(row=1, column=5, bookcase=bookcases[0]),
BookcaseShelf(row=2, column=5, bookcase=bookcases[0], description="/home"), BookcaseShelf(row=2, column=5, bookcase=bookcases[0], description="/home"),
BookcaseShelf(row=3, column=5, bookcase=bookcases[0], description="/home"), BookcaseShelf(row=3, column=5, bookcase=bookcases[0], description="/home"),
BookcaseShelf(row=0, column=0, bookcase=bookcases[1]), BookcaseShelf(row=0, column=0, bookcase=bookcases[1]),
BookcaseShelf(row=1, column=0, bookcase=bookcases[1], description="Kjellerarealer og komodovaraner"), BookcaseShelf(
row=1,
column=0,
bookcase=bookcases[1],
description="Kjellerarealer og komodovaraner",
),
BookcaseShelf(row=2, column=0, bookcase=bookcases[1]), BookcaseShelf(row=2, column=0, bookcase=bookcases[1]),
BookcaseShelf(row=3, column=0, bookcase=bookcases[1], description="Quick mafs"), BookcaseShelf(row=3, column=0, bookcase=bookcases[1], description="Quick mafs"),
BookcaseShelf(row=4, column=0, bookcase=bookcases[1]), BookcaseShelf(row=4, column=0, bookcase=bookcases[1]),
BookcaseShelf(row=0, column=0, bookcase=bookcases[2]), BookcaseShelf(row=0, column=0, bookcase=bookcases[2]),
BookcaseShelf(row=1, column=0, bookcase=bookcases[2]), BookcaseShelf(row=1, column=0, bookcase=bookcases[2]),
BookcaseShelf(row=2, column=0, bookcase=bookcases[2], description="AI"), BookcaseShelf(row=2, column=0, bookcase=bookcases[2], description="AI"),
BookcaseShelf(row=3, column=0, bookcase=bookcases[2], description="X86"), BookcaseShelf(row=3, column=0, bookcase=bookcases[2], description="X86"),
BookcaseShelf(row=4, column=0, bookcase=bookcases[2], description="Humanoira"), BookcaseShelf(row=4, column=0, bookcase=bookcases[2], description="Humanoira"),
BookcaseShelf(row=5, column=0, bookcase=bookcases[2], description="Hvem monterte rørforsterker?"), BookcaseShelf(
row=5,
column=0,
bookcase=bookcases[2],
description="Hvem monterte rørforsterker?",
),
BookcaseShelf(row=0, column=1, bookcase=bookcases[2]), BookcaseShelf(row=0, column=1, bookcase=bookcases[2]),
BookcaseShelf(row=1, column=1, bookcase=bookcases[2], description="Div data"), BookcaseShelf(row=1, column=1, bookcase=bookcases[2], description="Div data"),
BookcaseShelf(row=2, column=1, bookcase=bookcases[2], description="Chemistry"), BookcaseShelf(row=2, column=1, bookcase=bookcases[2], description="Chemistry"),
BookcaseShelf(row=3, column=1, bookcase=bookcases[2], description="Soviet Phys. Techn. Phys"), BookcaseShelf(
BookcaseShelf(row=4, column=1, bookcase=bookcases[2], description="Digitalteknikk"), row=3,
column=1,
bookcase=bookcases[2],
description="Soviet Phys. Techn. Phys",
),
BookcaseShelf(
row=4, column=1, bookcase=bookcases[2], description="Digitalteknikk"
),
BookcaseShelf(row=5, column=1, bookcase=bookcases[2], description="Material"), BookcaseShelf(row=5, column=1, bookcase=bookcases[2], description="Material"),
BookcaseShelf(row=0, column=2, bookcase=bookcases[2]), BookcaseShelf(row=0, column=2, bookcase=bookcases[2]),
BookcaseShelf(row=1, column=2, bookcase=bookcases[2], description="Assembler / APL"), BookcaseShelf(
row=1, column=2, bookcase=bookcases[2], description="Assembler / APL"
),
BookcaseShelf(row=2, column=2, bookcase=bookcases[2], description="Internet"), BookcaseShelf(row=2, column=2, bookcase=bookcases[2], description="Internet"),
BookcaseShelf(row=3, column=2, bookcase=bookcases[2], description="Algorithms"), BookcaseShelf(row=3, column=2, bookcase=bookcases[2], description="Algorithms"),
BookcaseShelf(row=4, column=2, bookcase=bookcases[2], description="Soviet Physics Jetp"), BookcaseShelf(
BookcaseShelf(row=5, column=2, bookcase=bookcases[2], description="Død og pine"), row=4, column=2, bookcase=bookcases[2], description="Soviet Physics Jetp"
),
BookcaseShelf(
row=5, column=2, bookcase=bookcases[2], description="Død og pine"
),
BookcaseShelf(row=0, column=3, bookcase=bookcases[2]), BookcaseShelf(row=0, column=3, bookcase=bookcases[2]),
BookcaseShelf(row=1, column=3, bookcase=bookcases[2], description="Web"), BookcaseShelf(row=1, column=3, bookcase=bookcases[2], description="Web"),
BookcaseShelf(row=2, column=3, bookcase=bookcases[2], description="Div languages"), BookcaseShelf(
row=2, column=3, bookcase=bookcases[2], description="Div languages"
),
BookcaseShelf(row=3, column=3, bookcase=bookcases[2], description="Python"), BookcaseShelf(row=3, column=3, bookcase=bookcases[2], description="Python"),
BookcaseShelf(row=4, column=3, bookcase=bookcases[2], description="D&D Minis"), BookcaseShelf(row=4, column=3, bookcase=bookcases[2], description="D&D Minis"),
BookcaseShelf(row=5, column=3, bookcase=bookcases[2], description="Perl"), BookcaseShelf(row=5, column=3, bookcase=bookcases[2], description="Perl"),
BookcaseShelf(row=0, column=4, bookcase=bookcases[2]), BookcaseShelf(row=0, column=4, bookcase=bookcases[2]),
BookcaseShelf(row=1, column=4, bookcase=bookcases[2], description="Knuth on programming"), BookcaseShelf(
BookcaseShelf(row=2, column=4, bookcase=bookcases[2], description="Div languages"), row=1, column=4, bookcase=bookcases[2], description="Knuth on programming"
BookcaseShelf(row=3, column=4, bookcase=bookcases[2], description="Typesetting"), ),
BookcaseShelf(
row=2, column=4, bookcase=bookcases[2], description="Div languages"
),
BookcaseShelf(
row=3, column=4, bookcase=bookcases[2], description="Typesetting"
),
BookcaseShelf(row=4, column=4, bookcase=bookcases[2]), BookcaseShelf(row=4, column=4, bookcase=bookcases[2]),
BookcaseShelf(row=0, column=0, bookcase=bookcases[3]), BookcaseShelf(row=0, column=0, bookcase=bookcases[3]),
BookcaseShelf(row=0, column=1, bookcase=bookcases[3]), BookcaseShelf(row=0, column=1, bookcase=bookcases[3]),
BookcaseShelf(row=0, column=2, bookcase=bookcases[3]), BookcaseShelf(row=0, column=2, bookcase=bookcases[3]),
BookcaseShelf(row=0, column=3, bookcase=bookcases[3]), BookcaseShelf(row=0, column=3, bookcase=bookcases[3]),
BookcaseShelf(row=0, column=4, bookcase=bookcases[3]), BookcaseShelf(row=0, column=4, bookcase=bookcases[3]),
BookcaseShelf(row=0, column=0, bookcase=bookcases[4]), BookcaseShelf(row=0, column=0, bookcase=bookcases[4]),
BookcaseShelf(row=0, column=1, bookcase=bookcases[4]), BookcaseShelf(row=0, column=1, bookcase=bookcases[4]),
BookcaseShelf(row=0, column=2, bookcase=bookcases[4]), BookcaseShelf(row=0, column=2, bookcase=bookcases[4]),
@ -132,24 +167,24 @@ def seed_data(sql_session: Session = db.session):
] ]
book1 = BookcaseItem( book1 = BookcaseItem(
name = "The Art of Computer Programming", name="The Art of Computer Programming",
isbn = "9780201896831", isbn="9780201896831",
) )
book1.authors.add(authors[0]) book1.authors.add(authors[0])
book1.media_type = media_types[0] book1.media_type = media_types[0]
book1.shelf = shelfs[59] book1.shelf = shelfs[59]
book2 = BookcaseItem( book2 = BookcaseItem(
name = "Harry Potter and the Philosopher's Stone", name="Harry Potter and the Philosopher's Stone",
isbn = "9780747532743", isbn="9780747532743",
) )
book2.authors.add(authors[1]) book2.authors.add(authors[1])
book2.media_type = media_types[0] book2.media_type = media_types[0]
book2.shelf = shelfs[-1] book2.shelf = shelfs[-1]
book_owned_by_other_user = BookcaseItem( book_owned_by_other_user = BookcaseItem(
name = "Book owned by other user", name="Book owned by other user",
isbn = "9780747532744", isbn="9780747532744",
) )
book_owned_by_other_user.owner = "other_user" book_owned_by_other_user.owner = "other_user"
@ -158,8 +193,8 @@ def seed_data(sql_session: Session = db.session):
book_owned_by_other_user.shelf = shelfs[-2] book_owned_by_other_user.shelf = shelfs[-2]
borrowed_book_more_available = BookcaseItem( borrowed_book_more_available = BookcaseItem(
name = "Borrowed book with more available", name="Borrowed book with more available",
isbn = "9780747532745", isbn="9780747532745",
) )
borrowed_book_more_available.authors.add(authors[5]) borrowed_book_more_available.authors.add(authors[5])
borrowed_book_more_available.media_type = media_types[0] borrowed_book_more_available.media_type = media_types[0]
@ -167,24 +202,24 @@ def seed_data(sql_session: Session = db.session):
borrowed_book_more_available.amount = 2 borrowed_book_more_available.amount = 2
borrowed_book_no_more_available = BookcaseItem( borrowed_book_no_more_available = BookcaseItem(
name = "Borrowed book with no more available", name="Borrowed book with no more available",
isbn = "9780747532746", isbn="9780747532746",
) )
borrowed_book_no_more_available.authors.add(authors[5]) borrowed_book_no_more_available.authors.add(authors[5])
borrowed_book_no_more_available.media_type = media_types[0] borrowed_book_no_more_available.media_type = media_types[0]
borrowed_book_no_more_available.shelf = shelfs[-3] borrowed_book_no_more_available.shelf = shelfs[-3]
borrowed_book_people_in_queue = BookcaseItem( borrowed_book_people_in_queue = BookcaseItem(
name = "Borrowed book with people in queue", name="Borrowed book with people in queue",
isbn = "9780747532747", isbn="9780747532747",
) )
borrowed_book_people_in_queue.authors.add(authors[5]) borrowed_book_people_in_queue.authors.add(authors[5])
borrowed_book_people_in_queue.media_type = media_types[0] borrowed_book_people_in_queue.media_type = media_types[0]
borrowed_book_people_in_queue.shelf = shelfs[-3] borrowed_book_people_in_queue.shelf = shelfs[-3]
borrowed_book_by_slabbedask = BookcaseItem( borrowed_book_by_slabbedask = BookcaseItem(
name = "Borrowed book by slabbedask", name="Borrowed book by slabbedask",
isbn = "9780747532748", isbn="9780747532748",
) )
borrowed_book_by_slabbedask.authors.add(authors[5]) borrowed_book_by_slabbedask.authors.add(authors[5])
borrowed_book_by_slabbedask.media_type = media_types[0] borrowed_book_by_slabbedask.media_type = media_types[0]
@ -216,9 +251,9 @@ def seed_data(sql_session: Session = db.session):
BookcaseItemBorrowingQueue(username="user", item=borrowed_book_people_in_queue), BookcaseItemBorrowingQueue(username="user", item=borrowed_book_people_in_queue),
] ]
with open(Path(__file__).parent.parent.parent / 'data' / 'iso639_1.csv') as f: with open(Path(__file__).parent.parent.parent / "data" / "iso639_1.csv") as f:
reader = csv.reader(f) reader = csv.reader(f)
languages = [Language(name, code) for (code, name) in reader] languages = [Language(name, code) for (code, name) in reader]
sql_session.add_all(media_types) sql_session.add_all(media_types)
sql_session.add_all(bookcases) sql_session.add_all(bookcases)
@ -229,4 +264,4 @@ def seed_data(sql_session: Session = db.session):
sql_session.add_all(borrowings) sql_session.add_all(borrowings)
sql_session.add_all(queue) sql_session.add_all(queue)
sql_session.commit() sql_session.commit()
print("Added test media types, bookcases and shelfs.") print("Added test media types, bookcases and shelfs.")