treewide: types, types and more types

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