From 3bab62b3acd3de2de8417694fff85d97f19389df Mon Sep 17 00:00:00 2001 From: h7x4 Date: Sun, 25 Jan 2026 21:37:35 +0900 Subject: [PATCH] treewide: types, types and more types --- dibbler/conf.py | 13 +- dibbler/lib/helpers.py | 47 ++++-- dibbler/lib/statistikkHelpers.py | 5 + dibbler/menus/addstock.py | 10 +- dibbler/menus/buymenu.py | 29 +++- dibbler/menus/editing.py | 13 +- dibbler/menus/faq.py | 31 +++- dibbler/menus/helpermenus.py | 272 +++++++++++++++++++++---------- dibbler/menus/mainmenu.py | 8 +- dibbler/menus/miscmenus.py | 19 ++- dibbler/menus/printermenu.py | 2 +- dibbler/menus/stats.py | 8 +- dibbler/models/Product.py | 11 +- dibbler/models/Purchase.py | 14 +- dibbler/models/Transaction.py | 13 +- dibbler/models/User.py | 12 +- dibbler/subcommands/loop.py | 43 ++++- 17 files changed, 385 insertions(+), 165 deletions(-) diff --git a/dibbler/conf.py b/dibbler/conf.py index 4e29514..5222808 100644 --- a/dibbler/conf.py +++ b/dibbler/conf.py @@ -4,7 +4,8 @@ import tomllib import os import sys -DEFAULT_CONFIG_PATH = Path('/etc/dibbler/dibbler.toml') +DEFAULT_CONFIG_PATH = Path("/etc/dibbler/dibbler.toml") + def default_config_path_submissive_and_readable() -> bool: return DEFAULT_CONFIG_PATH.is_file() and any( @@ -21,8 +22,10 @@ def default_config_path_submissive_and_readable() -> bool: ] ) + config: dict[str, dict[str, Any]] = dict() + def load_config(config_path: Path | None = None): global config if config_path is not None: @@ -32,9 +35,13 @@ def load_config(config_path: Path | None = None): with DEFAULT_CONFIG_PATH.open("rb") as file: config = tomllib.load(file) else: - print("Could not read config file, it was neither provided nor readable in default location", file=sys.stderr) + print( + "Could not read config file, it was neither provided nor readable in default location", + file=sys.stderr, + ) sys.exit(1) + def config_db_string() -> str: db_type = config["database"]["type"] @@ -54,7 +61,7 @@ def config_db_string() -> str: elif "password" in config["database"]["postgresql"]: password = config["database"]["postgresql"]["password"] else: - password = '' + password = "" if host.startswith("/"): return f"postgresql+psycopg2://{username}:{password}@/{dbname}?host={host}" diff --git a/dibbler/lib/helpers.py b/dibbler/lib/helpers.py index ed04de4..23d9066 100644 --- a/dibbler/lib/helpers.py +++ b/dibbler/lib/helpers.py @@ -1,15 +1,21 @@ -import pwd -import subprocess import os +import pwd import signal +import subprocess +from typing import Any, Callable, Literal -from sqlalchemy import or_, and_ +from sqlalchemy import and_, not_, or_ from sqlalchemy.orm import Session -from ..models import User, Product +from ..models import Product, User -def search_user(string, sql_session: Session, ignorethisflag=None): +def search_user( + string: str, + sql_session: Session, + ignorethisflag=None, +): + assert sql_session is not None string = string.lower() exact_match = ( sql_session.query(User) @@ -32,7 +38,12 @@ def search_user(string, sql_session: Session, ignorethisflag=None): return user_list -def search_product(string, sql_session: Session, find_hidden_products=True): +def search_product( + string: str, + sql_session: Session, + find_hidden_products: bool = True, +): + assert sql_session is not None if find_hidden_products: exact_match = ( sql_session.query(Product) @@ -45,7 +56,10 @@ def search_product(string, sql_session: Session, find_hidden_products=True): .filter( or_( Product.bar_code == string, - and_(Product.name == string, Product.hidden is False), + and_( + Product.name == string, + not_(Product.hidden), + ), ) ) .first() @@ -65,11 +79,14 @@ def search_product(string, sql_session: Session, find_hidden_products=True): ) else: product_list = ( - sql_ession.query(Product) + sql_session.query(Product) .filter( or_( Product.bar_code.ilike(f"%{string}%"), - and_(Product.name.ilike(f"%{string}%"), Product.hidden is False), + and_( + Product.name.ilike(f"%{string}%"), + not_(Product.hidden), + ), ) ) .all() @@ -77,7 +94,7 @@ def search_product(string, sql_session: Session, find_hidden_products=True): return product_list -def system_user_exists(username): +def system_user_exists(username: str) -> bool: try: pwd.getpwnam(username) except KeyError: @@ -88,7 +105,7 @@ def system_user_exists(username): return True -def guess_data_type(string): +def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "username"] | None: if string.startswith("ntnu") and string[4:].isdigit(): return "card" if string.isdigit() and len(string) == 10: @@ -102,7 +119,11 @@ def guess_data_type(string): return None -def argmax(d, all=False, value=None): +def argmax( + d, + all: bool = False, + value: Callable[[Any], Any] | None = None, +): maxarg = None if value is not None: dd = d @@ -117,7 +138,7 @@ def argmax(d, all=False, value=None): return maxarg -def less(string): +def less(string: str) -> None: """ Run less with string as input; wait until it finishes. """ diff --git a/dibbler/lib/statistikkHelpers.py b/dibbler/lib/statistikkHelpers.py index cffaf52..efbe87b 100644 --- a/dibbler/lib/statistikkHelpers.py +++ b/dibbler/lib/statistikkHelpers.py @@ -11,6 +11,7 @@ from ..models import Transaction def getUser(sql_session: Session): + assert sql_session is not None while 1: string = input("user? ") user = search_user(string, sql_session) @@ -38,6 +39,7 @@ def getUser(sql_session: Session): def getProduct(sql_session: Session): + assert sql_session is not None while 1: string = input("product? ") product = search_product(string, sql_session) @@ -237,6 +239,7 @@ def addLineToDatabase(database, inputLine): def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session): + assert sql_session is not None sdate = input("enter start date (yyyy-mm-dd)? ") edate = input("enter end date (yyyy-mm-dd)? ") print("building database...") @@ -463,6 +466,7 @@ def printGlobal(database, dateLine, n): def alt4menuTextOnly(database, dateLine, sql_session: Session): + assert sql_session is not None n = 10 while 1: print( @@ -491,6 +495,7 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session): def statisticsTextOnly(sql_session: Session): + assert sql_session is not None inputType = 4 product = "" user = "" diff --git a/dibbler/menus/addstock.py b/dibbler/menus/addstock.py index a3130df..ee4ecd3 100644 --- a/dibbler/menus/addstock.py +++ b/dibbler/menus/addstock.py @@ -10,12 +10,13 @@ from dibbler.models import ( Transaction, User, ) + from .helpermenus import Menu class AddStockMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Add stock and adjust credit", sql_session=sql_session, uses_db=True) + super().__init__("Add stock and adjust credit", sql_session) self.help_text = """ Enter what you have bought for PVVVV here, along with your user name and how much money you're due in credits for the purchase when prompted.\n""" @@ -110,7 +111,12 @@ much money you're due in credits for the purchase when prompted.\n""" print(f"{self.products[product][0]}".rjust(width - len(product.name))) print(width * "-") - def add_thing_to_pending(self, thing, amount, price): + def add_thing_to_pending( + self, + thing: User | Product, + amount: int, + price: int, + ): if isinstance(thing, User): self.users.append(thing) elif thing in list(self.products.keys()): diff --git a/dibbler/menus/buymenu.py b/dibbler/menus/buymenu.py index 7d09be1..e764e1e 100644 --- a/dibbler/menus/buymenu.py +++ b/dibbler/menus/buymenu.py @@ -14,8 +14,11 @@ from .helpermenus import Menu class BuyMenu(Menu): + superfast_mode: bool + purchase: Purchase + def __init__(self, sql_session: Session): - Menu.__init__(self, "Buy", sql_session=sql_session, uses_db=True) + super().__init__("Buy", sql_session) self.superfast_mode = False self.help_text = """ Each purchase may contain one or more products and one or more buyers. @@ -27,7 +30,7 @@ addition, and you can type 'what' at any time to redisplay it. When finished, write an empty line to confirm the purchase.\n""" @staticmethod - def credit_check(user): + def credit_check(user: User): """ :param user: @@ -38,7 +41,11 @@ When finished, write an empty line to confirm the purchase.\n""" return user.credit > config["limits"]["low_credit_warning_limit"] - def low_credit_warning(self, user, timeout=False): + def low_credit_warning( + self, + user: User, + timeout: bool = False, + ): assert isinstance(user, User) print("***********************************************************************") @@ -70,7 +77,11 @@ When finished, write an empty line to confirm the purchase.\n""" else: return self.confirm(prompt=">", default=True) - def add_thing_to_purchase(self, thing, amount=1): + def add_thing_to_purchase( + self, + thing: User | Product, + amount: int = 1, + ) -> bool: if isinstance(thing, User): if thing.is_anonymous(): print("---------------------------------------------") @@ -79,7 +90,10 @@ When finished, write an empty line to confirm the purchase.\n""" print("---------------------------------------------") if not self.credit_check(thing): - if self.low_credit_warning(user=thing, timeout=self.superfast_mode): + if self.low_credit_warning( + user=thing, + timeout=self.superfast_mode, + ): Transaction(thing, purchase=self.purchase, penalty=2) else: return False @@ -94,7 +108,10 @@ When finished, write an empty line to confirm the purchase.\n""" PurchaseEntry(self.purchase, thing, amount) return True - def _execute(self, initial_contents=None): + def _execute( + self, + initial_contents: list[tuple[User | Product, int]] | None = None, + ): self.print_header() self.purchase = Purchase() self.exit_confirm_msg = None diff --git a/dibbler/menus/editing.py b/dibbler/menus/editing.py index bcb1991..2330db0 100644 --- a/dibbler/menus/editing.py +++ b/dibbler/menus/editing.py @@ -17,7 +17,7 @@ __all__ = [ class AddUserMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Add user", sql_session=sql_session, uses_db=True) + super().__init__("Add user", sql_session) def _execute(self): self.print_header() @@ -41,7 +41,7 @@ class AddUserMenu(Menu): class EditUserMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Edit user", sql_session=sql_session, uses_db=True) + super().__init__("Edit user", sql_session) self.help_text = """ The only editable part of a user is its card number and rfid. @@ -80,7 +80,7 @@ user, then rfid (write an empty line to remove the card number or rfid). class AddProductMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Add product", sql_session=sql_session, uses_db=True) + super().__init__("Add product", sql_session) def _execute(self): self.print_header() @@ -99,7 +99,7 @@ class AddProductMenu(Menu): class EditProductMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Edit product", sql_session=sql_session, uses_db=True) + super().__init__("Edit product", sql_session) def _execute(self): self.print_header() @@ -108,6 +108,7 @@ class EditProductMenu(Menu): while True: selector = Selector( f"Do what with {product.name}?", + sql_session=self.sql_session, items=[ ("name", "Edit name"), ("price", "Edit price"), @@ -152,7 +153,7 @@ class EditProductMenu(Menu): class AdjustStockMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Adjust stock", sql_session=sql_session, uses_db=True) + super().__init__("Adjust stock", sql_session) def _execute(self): self.print_header() @@ -182,7 +183,7 @@ class AdjustStockMenu(Menu): class CleanupStockMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Stock Cleanup", sql_session=sql_session, uses_db=True) + super().__init__("Stock Cleanup", sql_session) def _execute(self): self.print_header() diff --git a/dibbler/menus/faq.py b/dibbler/menus/faq.py index 7563994..e43cb89 100644 --- a/dibbler/menus/faq.py +++ b/dibbler/menus/faq.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- +from sqlalchemy.orm import Session -from .helpermenus import MessageMenu, Menu +from .helpermenus import Menu, MessageMenu class FAQMenu(Menu): - def __init__(self): - Menu.__init__(self, "Frequently Asked Questions") + def __init__(self, sql_session: Session): + super().__init__("Frequently Asked Questions", sql_session) self.items = [ MessageMenu( "What is the meaning with this program?", @@ -17,19 +18,25 @@ class FAQMenu(Menu): Dibbler stores a "credit" amount for each user. When you register a purchase in Dibbler, this amount is decreased. To increase your - credit, purchase products for dibbler, and register them using "Add - stock and adjust credit". + credit, purchase products for dibbler, and register them using "Add + stock and adjust credit". Alternatively, add money to the money box and use "Adjust credit" to tell Dibbler about it. """, + sql_session, ), MessageMenu( "Can I still pay for stuff using cash?", """ - Please put money in the money box and use "Adjust Credit" so that + Please put money in the money box and use "Adjust Credit" so that dibbler can keep track of credit and purchases.""", + sql_session, + ), + MessageMenu( + "How do I exit from a submenu/dialog/thing?", + 'Type "exit", "q", or ^d.', + sql_session, ), - MessageMenu("How do I exit from a submenu/dialog/thing?", 'Type "exit", "q", or ^d.'), MessageMenu( 'What does "." mean?', """ @@ -41,6 +48,7 @@ class FAQMenu(Menu): line containing only a period, you should read the lines above and then press enter to continue. """, + sql_session, ), MessageMenu( "Why is the user interface so terribly unintuitive?", @@ -52,25 +60,30 @@ class FAQMenu(Menu): Answer #3: YOU are unintuitive. """, + sql_session, ), MessageMenu( "Why is there no help command?", 'There is. Have you tried typing "help"?', + sql_session, ), MessageMenu( 'Where are the easter eggs? I tried saying "moo", but nothing happened.', 'Don\'t say "moo".', + sql_session, ), MessageMenu( "Why does the program speak English when all the users are Norwegians?", "Godt spørsmål. Det virket sikkert som en god idé der og da.", + sql_session, ), MessageMenu( "Why does the screen have strange colours?", """ - Type "c" on the main menu to change the colours of the display, or + Type "c" on the main menu to change the colours of the display, or "cs" if you are a boring person. """, + sql_session, ), MessageMenu( "I found a bug; is there a reward?", @@ -101,6 +114,7 @@ class FAQMenu(Menu): 6. Type "restart" in Dibbler to replace the running process by a new one using the updated files. """, + sql_session, ), MessageMenu( "My question isn't listed here; what do I do?", @@ -125,5 +139,6 @@ class FAQMenu(Menu): 5. Type "restart" in Dibbler to replace the running process by a new one using the updated files. """, + sql_session, ), ] diff --git a/dibbler/menus/helpermenus.py b/dibbler/menus/helpermenus.py index ff9a0b7..c0f7ac6 100644 --- a/dibbler/menus/helpermenus.py +++ b/dibbler/menus/helpermenus.py @@ -1,46 +1,57 @@ # -*- coding: utf-8 -*- - - import re import sys from select import select +from typing import Any, Callable, Iterable, Literal, Self from sqlalchemy.orm import Session -from dibbler.models import User from dibbler.lib.helpers import ( - search_user, - search_product, - guess_data_type, argmax, + guess_data_type, + search_product, + search_user, ) +from dibbler.models import Product, User -exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"] -help_commands = ["help", "?"] -context_commands = ["what", "??"] -local_help_commands = ["help!", "???"] +exit_commands: list[str] = ["exit", "abort", "quit", "bye", "eat flaming death", "q"] +help_commands: list[str] = ["help", "?"] +context_commands: list[str] = ["what", "??"] +local_help_commands: list[str] = ["help!", "???"] -class ExitMenu(Exception): +class ExitMenuException(Exception): pass class Menu(object): + name: str + sql_session: Session + items: list[Self | tuple | str] + prompt: str | None + end_prompt: str | None + return_index: bool + exit_msg: str | None + exit_confirm_msg: str | None + exit_disallowed_msg: str | None + help_text: str | None + context: str | None + def __init__( self, - name, - items=None, - prompt=None, - end_prompt="> ", - return_index=True, - exit_msg=None, - exit_confirm_msg=None, - exit_disallowed_msg=None, - help_text=None, - uses_db=False, - sql_session: Session | None=None, + name: str, + sql_session: Session, + items: list[Self | tuple[Any, str] | str] | None = None, + prompt: str | None = None, + end_prompt: str | None = "> ", + return_index: bool = True, + exit_msg: str | None = None, + exit_confirm_msg: str | None = None, + exit_disallowed_msg: str | None = None, + help_text: str | None = None, ): - self.name = name + self.name: str = name + self.sql_session: Session = sql_session self.items = items if items is not None else [] self.prompt = prompt self.end_prompt = end_prompt @@ -50,48 +61,54 @@ class Menu(object): self.exit_disallowed_msg = exit_disallowed_msg self.help_text = help_text self.context = None - self.uses_db = uses_db - self.sql_session: Session | None = sql_session - assert not (self.uses_db and self.sql_session is None) + assert name is not None + assert self.sql_session is not None - def exit_menu(self): + def exit_menu(self) -> None: if self.exit_disallowed_msg is not None: print(self.exit_disallowed_msg) return if self.exit_confirm_msg is not None: if not self.confirm(self.exit_confirm_msg, default=True): return - raise ExitMenu() + raise ExitMenuException() - def at_exit(self): + def at_exit(self) -> None: if self.exit_msg: print(self.exit_msg) - def set_context(self, string, display=True): + def set_context( + self, + string: str | None, + display: bool = True, + ) -> None: self.context = string if self.context is not None and display: print(self.context) - def add_to_context(self, string): - self.context += string + def add_to_context(self, string: str) -> None: + if self.context is not None: + self.context += string + else: + self.context = string - def printc(self, string): + def printc(self, string: str) -> None: print(string) if self.context is None: self.context = string else: self.context += "\n" + string - def show_context(self): + def show_context(self) -> None: print(self.header()) if self.context is not None: print(self.context) - def item_is_submenu(self, i): + def item_is_submenu(self, i: int) -> bool: return isinstance(self.items[i], Menu) - def item_name(self, i): + def item_name(self, i: int) -> str: if self.item_is_submenu(i): return self.items[i].name elif isinstance(self.items[i], tuple): @@ -99,7 +116,7 @@ class Menu(object): else: return self.items[i] - def item_value(self, i): + def item_value(self, i: int): if isinstance(self.items[i], tuple): return self.items[i][0] if self.return_index: @@ -108,11 +125,11 @@ class Menu(object): def input_str( self, - prompt=None, - end_prompt=None, - regex=None, + prompt: str | None = None, + end_prompt: str | None = None, + regex: str | None = None, length_range=(None, None), - empty_string_is_none=False, + empty_string_is_none: bool = False, timeout=None, default=None, ): @@ -181,7 +198,7 @@ class Menu(object): continue return result - def special_input_options(self, result): + def special_input_options(self, result) -> bool: """ Handles special, magic input for input_str @@ -191,7 +208,7 @@ class Menu(object): """ return False - def special_input_choice(self, in_str): + def special_input_choice(self, in_str: str) -> bool: """ Handle choices which are not simply menu items. @@ -201,7 +218,12 @@ class Menu(object): """ return False - def input_choice(self, number_of_choices, prompt=None, end_prompt=None): + def input_choice( + self, + number_of_choices: int, + prompt: str | None = None, + end_prompt: str | None = None, + ): while True: result = self.input_str(prompt, end_prompt) if result == "": @@ -216,7 +238,7 @@ class Menu(object): if not self.special_input_choice(result): self.invalid_menu_choice(result) - def invalid_menu_choice(self, in_str): + def invalid_menu_choice(self, in_str: str): print("Please enter a valid choice.") def input_int( @@ -256,32 +278,40 @@ class Menu(object): except ValueError: print("Please enter an integer") - def input_user(self, prompt=None, end_prompt=None): + def input_user( + self, + prompt: str | None = None, + end_prompt: str | None = None, + ) -> User: user = None while user is None: user = self.retrieve_user(self.input_str(prompt, end_prompt)) return user - def retrieve_user(self, search_str): + def retrieve_user(self, search_str: str) -> User | None: return self.search_ui(search_user, search_str, "user") - def input_product(self, prompt=None, end_prompt=None): + def input_product( + self, + prompt: str | None = None, + end_prompt: str | None = None, + ) -> Product: product = None while product is None: product = self.retrieve_product(self.input_str(prompt, end_prompt)) return product - def retrieve_product(self, search_str): + def retrieve_product(self, search_str: str) -> Product | None: return self.search_ui(search_product, search_str, "product") def input_thing( self, - prompt=None, - end_prompt=None, - permitted_things=("user", "product"), + prompt: str | None = None, + end_prompt: str | None = None, + permitted_things: Iterable[str] = ("user", "product"), add_nonexisting=(), - empty_input_permitted=False, - find_hidden_products=True, + empty_input_permitted: bool = False, + find_hidden_products: bool = True, ): result = None while result is None: @@ -289,19 +319,22 @@ class Menu(object): if search_str == "" and empty_input_permitted: return None result = self.search_for_thing( - search_str, permitted_things, add_nonexisting, find_hidden_products + search_str, + permitted_things, + add_nonexisting, + find_hidden_products, ) return result def input_multiple( self, - prompt=None, - end_prompt=None, - permitted_things=("user", "product"), + prompt: str | None = None, + end_prompt: str | None = None, + permitted_things: Iterable[str] = ("user", "product"), add_nonexisting=(), - empty_input_permitted=False, - find_hidden_products=True, - ): + empty_input_permitted: bool = False, + find_hidden_products: bool = True, + ) -> tuple[User | Product, int] | None: result = None num = 0 while result is None: @@ -311,7 +344,10 @@ class Menu(object): return None else: result = self.search_for_thing( - search_str, permitted_things, add_nonexisting, find_hidden_products + search_str, + permitted_things, + add_nonexisting, + find_hidden_products, ) num = 1 @@ -333,12 +369,15 @@ class Menu(object): def search_for_thing( self, - search_str, + search_str: str, permitted_things=("user", "product"), add_non_existing=(), - find_hidden_products=True, - ): - search_fun = {"user": search_user, "product": search_product} + find_hidden_products: bool = True, + ) -> User | Product | None: + search_fun = { + "user": search_user, + "product": search_product, + } results = {} result_values = {} for thing in permitted_things: @@ -357,10 +396,14 @@ class Menu(object): return self.search_add(search_str) # print('No match found for "%s".' % search_str) return None - return self.search_ui2(search_str, results[selected_thing], selected_thing) + return self.search_ui2( + search_str, + results[selected_thing], + selected_thing, + ) @staticmethod - def search_result_value(result): + def search_result_value(result) -> Literal[0, 1, 2, 3]: if result is None: return 0 if not isinstance(result, list): @@ -371,7 +414,7 @@ class Menu(object): return 2 return 1 - def search_add(self, string): + def search_add(self, string: str) -> User | None: type_guess = guess_data_type(string) if type_guess == "username": print(f'"{string}" looks like a username, but no such user exists.') @@ -383,6 +426,7 @@ class Menu(object): if type_guess == "card": selector = Selector( f'"{string}" looks like a card number, but no user with that card number exists.', + self.sql_session, [ ("create", f"Create user with card number {string}"), ("set", f"Set card number of an existing user to {string}"), @@ -409,11 +453,21 @@ class Menu(object): print(f'"{string}" looks like the bar code for a product, but no such product exists.') return None - def search_ui(self, search_fun, search_str, thing): + def search_ui( + self, + search_fun: Callable[[str, Session], list[Any] | Any], + search_str: str, + thing: str, + ) -> Any: result = search_fun(search_str, self.sql_session) return self.search_ui2(search_str, result, thing) - def search_ui2(self, search_str, result, thing): + def search_ui2( + self, + search_str: str, + result: list[Any] | Any, + thing: str, + ) -> Any: if not isinstance(result, list): return result if len(result) == 0: @@ -433,21 +487,37 @@ class Menu(object): else: select_header = f'{len(result):d} {thing}s matching "{search_str}"' select_items = result - selector = Selector(select_header, items=select_items, return_index=False) + selector = Selector( + select_header, + self.sql_session, + items=select_items, + return_index=False, + ) return selector.execute() - @staticmethod - def confirm(prompt, end_prompt=None, default=None, timeout=None): - return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute() + def confirm( + self, + prompt: str, + end_prompt: str | None = None, + default: bool | None = None, + timeout: int | None = None, + ) -> bool: + return ConfirmMenu( + self.sql_session, + prompt, + end_prompt=None, + default=default, + timeout=timeout, + ).execute() - def header(self): + def header(self) -> str: return f"[{self.name}]" - def print_header(self): + def print_header(self) -> None: print("") print(self.header()) - def pause(self): + def pause(self) -> None: self.input_str(".", end_prompt="") @staticmethod @@ -489,7 +559,7 @@ class Menu(object): self.set_context(None) try: return self._execute(**kwargs) - except ExitMenu: + except ExitMenuException: self.at_exit() return None finally: @@ -515,8 +585,17 @@ class Menu(object): class MessageMenu(Menu): - def __init__(self, name, message, pause_after_message=True): - Menu.__init__(self, name) + message: str + pause_after_message: bool + + def __init__( + self, + name: str, + message: str, + sql_session: Session, + pause_after_message: bool = True, + ): + super().__init__(name, sql_session) self.message = message.strip() self.pause_after_message = pause_after_message @@ -529,10 +608,17 @@ class MessageMenu(Menu): class ConfirmMenu(Menu): - def __init__(self, prompt="confirm? ", end_prompt=": ", default=None, timeout=0): - Menu.__init__( - self, + def __init__( + self, + sql_session: Session, + prompt: str = "confirm? ", + end_prompt: str | None = ": ", + default: bool | None = None, + timeout: int | None = 0, + ): + super().__init__( "question", + sql_session, prompt=prompt, end_prompt=end_prompt, exit_disallowed_msg="Please answer yes or no", @@ -560,7 +646,8 @@ class ConfirmMenu(Menu): class Selector(Menu): def __init__( self, - name, + name: str, + sql_session: Session, items=None, prompt="select", return_index=True, @@ -570,15 +657,22 @@ class Selector(Menu): ): if items is None: items = [] - Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg) + super().__init__( + name, + sql_session, + items, + prompt, + return_index=return_index, + exit_msg=exit_msg, + ) - def header(self): + def header(self) -> str: return self.name - def print_header(self): + def print_header(self) -> None: print(self.header()) - def local_help(self): + def local_help(self) -> None: if self.help_text is None: print("This is a selection menu. Enter one of the listed numbers, or") print("'exit' to go out and do something else.") diff --git a/dibbler/menus/mainmenu.py b/dibbler/menus/mainmenu.py index a10d97e..8ed9925 100644 --- a/dibbler/menus/mainmenu.py +++ b/dibbler/menus/mainmenu.py @@ -19,7 +19,7 @@ def restart(): class MainMenu(Menu): - def special_input_choice(self, in_str): + def special_input_choice(self, in_str: str) -> bool: mv = in_str.split() if len(mv) == 2 and mv[0].isdigit(): num = int(mv[0]) @@ -35,9 +35,9 @@ class MainMenu(Menu): return True return False - def special_input_options(self, result): + def special_input_options(self, result: str) -> bool: if result in faq_commands: - FAQMenu().execute() + FAQMenu(self.sql_session).execute() return True if result in restart_commands: if self.confirm("Restart Dibbler?"): @@ -62,5 +62,5 @@ class MainMenu(Menu): return True return False - def invalid_menu_choice(self, in_str): + def invalid_menu_choice(self, in_str: str) -> None: print(self.show_context()) diff --git a/dibbler/menus/miscmenus.py b/dibbler/menus/miscmenus.py index 975b3fc..ea3e54c 100644 --- a/dibbler/menus/miscmenus.py +++ b/dibbler/menus/miscmenus.py @@ -10,7 +10,7 @@ from .helpermenus import Menu, Selector class TransferMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Transfer credit between users", sql_session=sql_session, uses_db=True) + super().__init__("Transfer credit between users", sql_session) def _execute(self): self.print_header() @@ -42,7 +42,7 @@ class TransferMenu(Menu): class ShowUserMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Show user", sql_session=sql_session, uses_db=True) + super().__init__("Show user", sql_session) def _execute(self): self.print_header() @@ -53,6 +53,7 @@ class ShowUserMenu(Menu): print(f"Credit: {user.credit} kr") selector = Selector( f"What do you want to know about {user.name}?", + self.sql_session, items=[ ( "transactions", @@ -75,7 +76,7 @@ class ShowUserMenu(Menu): print("What what?") @staticmethod - def print_transactions(user, limit=None): + def print_transactions(user: User, limit: int | None = None) -> None: num_trans = len(user.transactions) if limit is None: limit = num_trans @@ -99,13 +100,13 @@ class ShowUserMenu(Menu): string += ")" if t.penalty > 1: string += f" * {t.penalty:d}x penalty applied" - else: + elif t.description is not None: string += t.description string += "\n" less(string) @staticmethod - def print_purchased_products(user): + def print_purchased_products(user: User) -> None: products = [] for ref in user.products: product = ref.product @@ -125,7 +126,7 @@ class ShowUserMenu(Menu): class UserListMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "User list", sql_session=sql_session, uses_db=True) + super().__init__("User list", sql_session) def _execute(self): self.print_header() @@ -146,7 +147,7 @@ class UserListMenu(Menu): class AdjustCreditMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Adjust credit", sql_session=sql_session, uses_db=True) + super().__init__("Adjust credit", sql_session) def _execute(self): self.print_header() @@ -176,7 +177,7 @@ class AdjustCreditMenu(Menu): class ProductListMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Product list", sql_session=sql_session, uses_db=True) + super().__init__("Product list", sql_session) def _execute(self): self.print_header() @@ -206,7 +207,7 @@ class ProductListMenu(Menu): class ProductSearchMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Product search", sql_session=sql_session, uses_db=True) + super().__init__("Product search", sql_session) def _execute(self): self.print_header() diff --git a/dibbler/menus/printermenu.py b/dibbler/menus/printermenu.py index afe0c04..4a824ad 100644 --- a/dibbler/menus/printermenu.py +++ b/dibbler/menus/printermenu.py @@ -11,7 +11,7 @@ from .helpermenus import Menu class PrintLabelMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Print a label", sql_session=sql_session, uses_db=True) + super().__init__("Print a label", sql_session) self.help_text = """ Prints out a product bar code on the printer diff --git a/dibbler/menus/stats.py b/dibbler/menus/stats.py index ee0ed0f..d14f693 100644 --- a/dibbler/menus/stats.py +++ b/dibbler/menus/stats.py @@ -17,7 +17,7 @@ __all__ = [ class ProductPopularityMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Products by popularity", sql_session=sql_session, uses_db=True) + super().__init__("Products by popularity", sql_session) def _execute(self): self.print_header() @@ -50,7 +50,7 @@ class ProductPopularityMenu(Menu): class ProductRevenueMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Products by revenue", sql_session=sql_session, uses_db=True) + super().__init__("Products by revenue", sql_session) def _execute(self): self.print_header() @@ -88,7 +88,7 @@ class ProductRevenueMenu(Menu): class BalanceMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Total balance of PVVVV", sql_session=sql_session, uses_db=True) + super().__init__("Total balance of PVVVV", sql_session) def _execute(self): self.print_header() @@ -121,7 +121,7 @@ class BalanceMenu(Menu): class LoggedStatisticsMenu(Menu): def __init__(self, sql_session: Session): - Menu.__init__(self, "Statistics from log", sql_session=sql_session, uses_db=True) + super().__init__("Statistics from log", sql_session) def _execute(self): statisticsTextOnly(self.sql_session) diff --git a/dibbler/models/Product.py b/dibbler/models/Product.py index 48e2f26..463f0e7 100644 --- a/dibbler/models/Product.py +++ b/dibbler/models/Product.py @@ -36,12 +36,19 @@ class Product(Base): name_re = r".+" name_length = 45 - def __init__(self, bar_code, name, price, stock=0, hidden=False): + def __init__( + self, + bar_code: str, + name: str, + price: int, + stock: int = 0, + hidden: bool = False, + ): self.name = name self.bar_code = bar_code self.price = price self.stock = stock self.hidden = hidden - def __str__(self): + def __str__(self) -> str: return self.name diff --git a/dibbler/models/Purchase.py b/dibbler/models/Purchase.py index b725f96..42c4c52 100644 --- a/dibbler/models/Purchase.py +++ b/dibbler/models/Purchase.py @@ -36,16 +36,16 @@ class Purchase(Base): def __init__(self): pass - def is_complete(self): + def is_complete(self) -> bool: return len(self.transactions) > 0 and len(self.entries) > 0 - def price_per_transaction(self, round_up=True): + def price_per_transaction(self, round_up: bool = True) -> int: if round_up: return int(math.ceil(float(self.price) / len(self.transactions))) else: return int(math.floor(float(self.price) / len(self.transactions))) - def set_price(self, round_up=True): + def set_price(self, round_up: bool = True) -> None: self.price = 0 for entry in self.entries: self.price += entry.amount * entry.product.price @@ -53,16 +53,16 @@ class Purchase(Base): for t in self.transactions: t.amount = self.price_per_transaction(round_up=round_up) - def perform_purchase(self, ignore_penalty=False, round_up=True): - self.time = datetime.datetime.now() + def perform_purchase(self, ignore_penalty: bool = False, round_up: bool = True) -> None: + self.time = datetime.now() self.set_price(round_up=round_up) for t in self.transactions: t.perform_transaction(ignore_penalty=ignore_penalty) for entry in self.entries: entry.product.stock -= entry.amount - def perform_soft_purchase(self, price, round_up=True): - self.time = datetime.datetime.now() + def perform_soft_purchase(self, price: int, round_up: bool = True) -> None: + self.time = datetime.now() self.price = price for t in self.transactions: t.amount = self.price_per_transaction(round_up=round_up) diff --git a/dibbler/models/Transaction.py b/dibbler/models/Transaction.py index df1155c..6c509ef 100644 --- a/dibbler/models/Transaction.py +++ b/dibbler/models/Transaction.py @@ -38,15 +38,22 @@ class Transaction(Base): user: Mapped[User] = relationship(lazy="joined") purchase: Mapped[Purchase] = relationship(lazy="joined") - def __init__(self, user, amount=0, description=None, purchase=None, penalty=1): + def __init__( + self, + user: User, + amount: int = 0, + description: str | None = None, + purchase: Purchase | None = None, + penalty: int = 1, + ): self.user = user self.amount = amount self.description = description self.purchase = purchase self.penalty = penalty - def perform_transaction(self, ignore_penalty=False): - self.time = datetime.datetime.now() + def perform_transaction(self, ignore_penalty: bool = False) -> None: + self.time = datetime.now() if not ignore_penalty: self.amount *= self.penalty self.user.credit -= self.amount diff --git a/dibbler/models/User.py b/dibbler/models/User.py index d93e7fb..1fd91c1 100644 --- a/dibbler/models/User.py +++ b/dibbler/models/User.py @@ -25,14 +25,20 @@ class User(Base): card: Mapped[str | None] = mapped_column(String(20)) rfid: Mapped[str | None] = mapped_column(String(20)) - products: Mapped[set[UserProducts]] = relationship(back_populates="user") - transactions: Mapped[set[Transaction]] = relationship(back_populates="user") + products: Mapped[list[UserProducts]] = relationship(back_populates="user") + transactions: Mapped[list[Transaction]] = relationship(back_populates="user") name_re = r"[a-z]+" card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?" rfid_re = r"[0-9a-fA-F]*" - def __init__(self, name, card, rfid=None, credit=0): + def __init__( + self, + name: str, + card: str | None, + rfid: str | None = None, + credit: int = 0, + ): self.name = name if card == "": card = None diff --git a/dibbler/subcommands/loop.py b/dibbler/subcommands/loop.py index 61fc81a..10f0d9f 100755 --- a/dibbler/subcommands/loop.py +++ b/dibbler/subcommands/loop.py @@ -4,25 +4,56 @@ import random import sys import traceback +from signal import ( + SIG_IGN, + SIGQUIT, + SIGTSTP, +) +from signal import ( + signal as set_signal_handler, +) from sqlalchemy.orm import Session from ..conf import config -from ..lib.helpers import * -from ..menus import * +from ..menus import ( + AddProductMenu, + AddStockMenu, + AddUserMenu, + AdjustCreditMenu, + AdjustStockMenu, + BalanceMenu, + BuyMenu, + CleanupStockMenu, + EditProductMenu, + EditUserMenu, + FAQMenu, + LoggedStatisticsMenu, + MainMenu, + Menu, + PrintLabelMenu, + ProductListMenu, + ProductPopularityMenu, + ProductRevenueMenu, + ProductSearchMenu, + ShowUserMenu, + TransferMenu, + UserListMenu, +) random.seed() def main(sql_session: Session): if not config["general"]["stop_allowed"]: - signal.signal(signal.SIGQUIT, signal.SIG_IGN) + set_signal_handler(SIGQUIT, SIG_IGN) if not config["general"]["stop_allowed"]: - signal.signal(signal.SIGTSTP, signal.SIG_IGN) + set_signal_handler(SIGTSTP, SIG_IGN) main = MainMenu( "Dibbler main menu", + sql_session, items=[ BuyMenu(sql_session), ProductListMenu(sql_session), @@ -33,6 +64,7 @@ def main(sql_session: Session): AddStockMenu(sql_session), Menu( "Add/edit", + sql_session, items=[ AddUserMenu(sql_session), EditUserMenu(sql_session), @@ -45,6 +77,7 @@ def main(sql_session: Session): ProductSearchMenu(sql_session), Menu( "Statistics", + sql_session, items=[ ProductPopularityMenu(sql_session), ProductRevenueMenu(sql_session), @@ -52,7 +85,7 @@ def main(sql_session: Session): LoggedStatisticsMenu(sql_session), ], ), - FAQMenu(), + FAQMenu(sql_session), PrintLabelMenu(sql_session), ], exit_msg="happy happy joy joy",