diff --git a/dibbler/main.py b/dibbler/main.py index f7c459f..9962574 100644 --- a/dibbler/main.py +++ b/dibbler/main.py @@ -1,4 +1,8 @@ import argparse +from pathlib import Path + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session from dibbler.conf import config @@ -23,29 +27,62 @@ subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker") subparsers.add_parser("seed-data", help="Fill with mock data") +def _get_database_url_from_config() -> str: + """Get the database URL from the configuration.""" + url = config.get("database", "url") + if url is not None: + return url + + url_file = config.get("database", "url_file") + if url_file is not None: + with Path(url_file).open() as file: + return file.read().strip() + + raise ValueError("No database URL found in configuration.") + + +def _connect_to_database(url: str, **engine_args) -> Session: + try: + engine = create_engine(url, **engine_args) + sql_session = Session(engine) + except Exception as err: + print("Error: could not connect to database.") + print(err) + exit(1) + + print(f"Debug: Connected to database at '{url}'") + return sql_session + + def main(): args = parser.parse_args() config.read(args.config) + database_url = _get_database_url_from_config() + sql_session = _connect_to_database( + database_url, + echo=config.getboolean("database", "echo_sql", fallback=False), + ) + if args.subcommand == "loop": import dibbler.subcommands.loop as loop - loop.main() + loop.main(sql_session) elif args.subcommand == "create-db": import dibbler.subcommands.makedb as makedb - makedb.main() + makedb.main(sql_session) elif args.subcommand == "slabbedasker": import dibbler.subcommands.slabbedasker as slabbedasker - slabbedasker.main() + slabbedasker.main(sql_session) elif args.subcommand == "seed-data": import dibbler.subcommands.seed_test_data as seed_test_data - seed_test_data.main() + seed_test_data.main(sql_session) if __name__ == "__main__": diff --git a/dibbler/menus/main.py b/dibbler/menus/main.py new file mode 100644 index 0000000..9b95d34 --- /dev/null +++ b/dibbler/menus/main.py @@ -0,0 +1,176 @@ +import sys +import signal +import traceback + +from sqlalchemy import ( + event, +) +from sqlalchemy.orm import Session + +from libdib.repl import ( + NumberedCmd, + InteractiveItemSelector, + prompt_yes_no, +) + +from dibbler.conf import config + +class DibblerCli(NumberedCmd): + def __init__(self, sql_session: Session): + super().__init__() + self.sql_session = sql_session + self.sql_session_dirty = False + + @event.listens_for(self.sql_session, "after_flush") + def mark_session_as_dirty(*_): + self.sql_session_dirty = True + self.prompt_header = "(unsaved changes)" + + @event.listens_for(self.sql_session, "after_commit") + @event.listens_for(self.sql_session, "after_rollback") + def mark_session_as_clean(*_): + self.sql_session_dirty = False + self.prompt_header = None + + # TODO: move to libdib.repl + @classmethod + def run_with_safe_exit_wrapper(cls, sql_session: Session): + tool = cls(sql_session) + + if not config.getboolean("general", "stop_allowed"): + signal.signal(signal.SIGQUIT, signal.SIG_IGN) + + if not config.getboolean("general", "stop_allowed"): + signal.signal(signal.SIGTSTP, signal.SIG_IGN) + + while True: + try: + tool.cmdloop() + except KeyboardInterrupt: + if not tool.sql_session_dirty: + exit(0) + try: + print() + if prompt_yes_no( + "Are you sure you want to exit without saving?", default=False + ): + raise KeyboardInterrupt + except KeyboardInterrupt: + if tool.sql_session is not None: + tool.sql_session.rollback() + exit(0) + except Exception: + print("Something went wrong.") + print(f"{sys.exc_info()[0]}: {sys.exc_info()[1]}") + if config.getboolean("general", "show_tracebacks"): + traceback.print_tb(sys.exc_info()[2]) + + def default(self, maybe_barcode: str): + raise NotImplementedError( + "This command is not implemented yet. Please use the numbered commands instead." + ) + + def do_buy(self, arg: str): + ... + + def do_product_list(self, arg: str): + ... + + def do_show_user(self, arg: str): + ... + + def do_user_list(self, arg: str): + ... + + def do_adjust_credit(self, arg: str): + ... + + def do_transfer(self, arg: str): + ... + + def do_add_stock(self, arg: str): + ... + + def do_add_edit(self, arg: str): + ... + # AddEditMenu(self.sql_session).cmdloop() + + def do_product_search(self, arg: str): + ... + + def do_statistics(self, arg: str): + ... + + def do_faq(self, arg: str): + ... + + def do_print_label(self, arg: str): + ... + + + def do_exit(self, _: str): + if self.sql_session_dirty: + if prompt_yes_no("Would you like to save your changes?"): + self.sql_session.commit() + else: + self.sql_session.rollback() + exit(0) + + funcs = { + 0: { + "f": default, + "doc": "Choose / Add item with its ISBN", + }, + 1: { + "f": do_buy, + "doc": "Buy", + }, + 2: { + "f": do_product_list, + "doc": "Product List", + }, + 3: { + "f": do_show_user, + "doc": "Show User", + }, + 4: { + "f": do_user_list, + "doc": "User List", + }, + 5: { + "f": do_adjust_credit, + "doc": "Adjust Credit", + }, + 6: { + "f": do_transfer, + "doc": "Transfer", + }, + 7: { + "f": do_add_stock, + "doc": "Add Stock", + }, + 8: { + "f": do_add_edit, + "doc": "Add/Edit", + }, + 9: { + "f": do_product_search, + "doc": "Product Search", + }, + 10: { + "f": do_statistics, + "doc": "Statistics", + }, + 11: { + "f": do_faq, + "doc": "FAQ", + }, + 12: { + "f": do_print_label, + "doc": "Print Label", + }, + 13: { + "f": do_exit, + "doc": "Exit", + }, + } diff --git a/dibbler/subcommands/loop.py b/dibbler/subcommands/loop.py index e2a2f3c..66e4a15 100755 --- a/dibbler/subcommands/loop.py +++ b/dibbler/subcommands/loop.py @@ -1,79 +1,12 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - import random -import sys -import traceback -from ..conf import config -from ..lib.helpers import * -from ..menus import * +from sqlalchemy.orm import Session -random.seed() +from ..menus.main import DibblerCli +def main(sql_session: Session): + random.seed() -def main(): - if not config.getboolean("general", "stop_allowed"): - signal.signal(signal.SIGQUIT, signal.SIG_IGN) + DibblerCli.run_with_safe_exit_wrapper(sql_session) - if not config.getboolean("general", "stop_allowed"): - signal.signal(signal.SIGTSTP, signal.SIG_IGN) - - main = MainMenu( - "Dibbler main menu", - items=[ - BuyMenu(), - ProductListMenu(), - ShowUserMenu(), - UserListMenu(), - AdjustCreditMenu(), - TransferMenu(), - AddStockMenu(), - Menu( - "Add/edit", - items=[ - AddUserMenu(), - EditUserMenu(), - AddProductMenu(), - EditProductMenu(), - AdjustStockMenu(), - CleanupStockMenu(), - ], - ), - ProductSearchMenu(), - Menu( - "Statistics", - items=[ - ProductPopularityMenu(), - ProductRevenueMenu(), - BalanceMenu(), - LoggedStatisticsMenu(), - ], - ), - FAQMenu(), - PrintLabelMenu(), - ], - exit_msg="happy happy joy joy", - exit_confirm_msg="Really quit Dibbler?", - ) - if not config.getboolean("general", "quit_allowed"): - main.exit_disallowed_msg = "You can check out any time you like, but you can never leave." - while True: - # noinspection PyBroadException - try: - main.execute() - except KeyboardInterrupt: - print("") - print("Interrupted.") - except: - print("Something went wrong.") - print(f"{sys.exc_info()[0]}: {sys.exc_info()[1]}") - if config.getboolean("general", "show_tracebacks"): - traceback.print_tb(sys.exc_info()[2]) - else: - break - print("Restarting main menu.") - - -if __name__ == "__main__": - main() + exit(0) diff --git a/dibbler/subcommands/makedb.py b/dibbler/subcommands/makedb.py index 74a6826..d463bcd 100644 --- a/dibbler/subcommands/makedb.py +++ b/dibbler/subcommands/makedb.py @@ -1,11 +1,8 @@ -#!/usr/bin/python +from sqlalchemy.orm import Session from dibbler.models import Base -from dibbler.db import engine -def main(): - Base.metadata.create_all(engine) - - -if __name__ == "__main__": - main() +def main(sql_session: Session): + if not sql_session.bind: + raise RuntimeError("SQLAlchemy session is not bound to a database engine.") + Base.metadata.create_all(sql_session.bind) diff --git a/dibbler/subcommands/seed_test_data.py b/dibbler/subcommands/seed_test_data.py index 07454ea..af0b905 100644 --- a/dibbler/subcommands/seed_test_data.py +++ b/dibbler/subcommands/seed_test_data.py @@ -1,24 +1,21 @@ import json -from dibbler.db import Session - from pathlib import Path -from dibbler.models.Product import Product +from sqlalchemy.orm import Session -from dibbler.models.User import User +from dibbler.models import Product, User JSON_FILE = Path(__file__).parent.parent.parent / "mock_data.json" -def clear_db(session): - session.query(Product).delete() - session.query(User).delete() - session.commit() +def clear_db(sql_session: Session): + sql_session.query(Product).delete() + sql_session.query(User).delete() + sql_session.commit() -def main(): - session = Session() - clear_db(session) +def main(sql_session: Session): + clear_db(sql_session) product_items = [] user_items = [] @@ -43,6 +40,6 @@ def main(): ) user_items.append(user_item) - session.add_all(product_items) - session.add_all(user_items) - session.commit() + sql_session.add_all(product_items) + sql_session.add_all(user_items) + sql_session.commit() diff --git a/dibbler/subcommands/slabbedasker.py b/dibbler/subcommands/slabbedasker.py index a1a9df1..47fda8f 100644 --- a/dibbler/subcommands/slabbedasker.py +++ b/dibbler/subcommands/slabbedasker.py @@ -1,18 +1,12 @@ -#!/usr/bin/python +from sqlalchemy.orm import Session -from dibbler.db import Session +# from dibbler.db import Session from dibbler.models import User -def main(): - # Start an SQL session - session = Session() - # Let's find all users with a negative credit - slabbedasker = session.query(User).filter(User.credit < 0).all() +def main(sql_session: Session): + # Let's find all users with a negative credit + slabbedasker = sql_session.query(User).filter(User.credit < 0).all() for slubbert in slabbedasker: print(f"{slubbert.name}, {slubbert.credit}") - - -if __name__ == "__main__": - main() diff --git a/example-config.ini b/example-config.ini index 7abacb0..324b7fd 100644 --- a/example-config.ini +++ b/example-config.ini @@ -6,7 +6,7 @@ input_encoding = 'utf8' [database] # url = "postgresql://robertem@127.0.0.1/pvvvv" -url = "sqlite:///test.db" +url = sqlite:///test.db [limits] low_credit_warning_limit = -100 diff --git a/nix/dibbler.nix b/nix/dibbler.nix index acbbcf1..75124e1 100644 --- a/nix/dibbler.nix +++ b/nix/dibbler.nix @@ -1,6 +1,5 @@ { lib , python3Packages -, fetchFromGitHub }: python3Packages.buildPythonApplication { pname = "dibbler"; diff --git a/pyproject.toml b/pyproject.toml index 3179a6f..403d8eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,15 @@ dependencies = [ "SQLAlchemy >= 2.0, <2.1", "brother-ql", "matplotlib", + "libdib", "psycopg2 >= 2.8, <2.10", "python-barcode", ] dynamic = ["version"] +[tool.uv.sources] +libdib = { git = "https://git.pvv.ntnu.no/Projects/libdib.git" } + [tool.setuptools.packages.find] include = ["dibbler*"]