diff --git a/config-template.toml b/config-template.toml index 9dcd9e0..2f71f78 100644 --- a/config-template.toml +++ b/config-template.toml @@ -1,9 +1,6 @@ -# See https://flask.palletsprojects.com/en/2.3.x/config/ -[flask] -TESTING = true -DEBUG = true -FLASK_ENV = 'development' -SECRET_KEY = 'change-me' +[logging] +debug = true +debug_sql = false [database] # One of (sqlite, postgres) @@ -16,5 +13,13 @@ path = './worblehat.sqlite' host = 'localhost' port = 5432 username = 'worblehat' -password = 'change-me' +password = '/var/lib/worblehat/db-password' # path or plain text name = 'worblehat' + +# See https://flask.palletsprojects.com/en/2.3.x/config/ +[flask] +TESTING = true +DEBUG = true +FLASK_ENV = 'development' +SECRET_KEY = 'change-me' # path or plain text + diff --git a/flake.nix b/flake.nix index 300d064..cb39885 100644 --- a/flake.nix +++ b/flake.nix @@ -13,9 +13,8 @@ inherit program; }; in { - default = self.apps.${system}.dev; - dev = app "${self.packages.${system}.worblehat}/bin/dev"; - cli = app "${self.packages.${system}.worblehat}/bin/cli"; + default = self.apps.${system}.worblehat; + worblehat = app "${self.packages.${system}.worblehat}/bin/worblehat"; }; packages.${system} = { diff --git a/pyproject.toml b/pyproject.toml index 57a527d..a06ec1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,7 @@ werkzeug = "^2.3.3" poethepoet = "^0.20.0" [tool.poetry.scripts] -cli = "worblehat.cli.main:main" -dev = "worblehat.flaskapp.wsgi_dev:main" +worblehat = "worblehat.main:main" [tool.poe.tasks] clean = """ diff --git a/worblehat/cli/__init__.py b/worblehat/cli/__init__.py index e69de29..b55ba85 100644 --- a/worblehat/cli/__init__.py +++ b/worblehat/cli/__init__.py @@ -0,0 +1 @@ +from .main import WorblehatCli \ No newline at end of file diff --git a/worblehat/cli/main.py b/worblehat/cli/main.py index e49ffb6..277d9b3 100644 --- a/worblehat/cli/main.py +++ b/worblehat/cli/main.py @@ -1,19 +1,15 @@ from textwrap import dedent from sqlalchemy import ( - create_engine, event, select, ) -from sqlalchemy.orm import ( - Session, -) -from worblehat.services.bookcase_item import ( +from sqlalchemy.orm import Session + +from worblehat.services import ( create_bookcase_item_from_isbn, is_valid_isbn, ) -from worblehat.services.config import Config -from worblehat.services.argument_parser import parse_args from worblehat.models import * @@ -29,19 +25,10 @@ from .subclis import ( # the shelves? class WorblehatCli(NumberedCmd): - sql_session: Session - sql_session_dirty: bool = False - - def __init__(self, args: dict[str, any] | None = None): + def __init__(self, sql_session: Session): super().__init__() - - try: - engine = create_engine(Config.db_string(), echo=args.get('verbose_sql', False)) - self.sql_session = Session(engine) - except Exception as err: - print('Error: could not connect to database.') - print(err) - exit(1) + self.sql_session = sql_session + self.sql_session_dirty = False @event.listens_for(self.sql_session, 'after_flush') def mark_session_as_dirty(*_): @@ -54,7 +41,23 @@ class WorblehatCli(NumberedCmd): self.sql_session_dirty = False self.prompt_header = None - print(f"Debug: Connected to database at '{Config.db_string()}'") + @classmethod + def run_with_safe_exit_wrapper(cls, sql_session: Session): + tool = cls(sql_session) + 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) def do_list_bookcases(self, _: str): @@ -221,28 +224,4 @@ class WorblehatCli(NumberedCmd): 'f': do_exit, 'doc': 'Exit', }, - } - - -def main(): - args = parse_args() - Config.load_configuration(args) - - tool = WorblehatCli(args) - 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) - -if __name__ == '__main__': - main() \ No newline at end of file + } \ No newline at end of file diff --git a/worblehat/flaskapp/flaskapp.py b/worblehat/flaskapp/flaskapp.py index fe0fa9e..fd32147 100644 --- a/worblehat/flaskapp/flaskapp.py +++ b/worblehat/flaskapp/flaskapp.py @@ -13,13 +13,10 @@ from .database import db def create_app(args: dict[str, any] | None = None): app = Flask(__name__) - if args is not None: - Config.load_configuration(args) - print(Config.db_string()) - app.config.update(Config['flask']) - app.config.update(Config._config) - app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string() - app.config['SQLALCHEMY_ECHO'] = args.get('verbose_sql') + app.config.update(Config['flask']) + app.config.update(Config._config) + app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string() + app.config['SQLALCHEMY_ECHO'] = Config['logging.debug_sql'] db.init_app(app) diff --git a/worblehat/flaskapp/wsgi_dev.py b/worblehat/flaskapp/wsgi_dev.py index 739d773..4dbab90 100644 --- a/worblehat/flaskapp/wsgi_dev.py +++ b/worblehat/flaskapp/wsgi_dev.py @@ -1,13 +1,11 @@ from werkzeug import run_simple from worblehat.services.config import Config -from worblehat.services.argument_parser import parse_args from .flaskapp import create_app def main(): - args = parse_args() - app = create_app(args) + app = create_app() run_simple( hostname = 'localhost', port = 5000, diff --git a/worblehat/flaskapp/wsgi_prod.py b/worblehat/flaskapp/wsgi_prod.py index a9eb58c..7f64acc 100644 --- a/worblehat/flaskapp/wsgi_prod.py +++ b/worblehat/flaskapp/wsgi_prod.py @@ -1,3 +1,8 @@ from .flaskapp import create_app -app = create_app() +def main(): + app = create_app() + app.run() + +if __name__ == '__main__': + main() diff --git a/worblehat/main.py b/worblehat/main.py new file mode 100644 index 0000000..e5c6522 --- /dev/null +++ b/worblehat/main.py @@ -0,0 +1,67 @@ +import logging +from pprint import pformat + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from .services import ( + Config, + arg_parser, +) + +from .cli import WorblehatCli +from .flaskapp.wsgi_dev import main as flask_dev_main +from .flaskapp.wsgi_prod import main as flask_prod_main + + +def _print_version() -> None: + from worblehat import __version__ + print(f'Worblehat version {__version__}') + + +def _connect_to_database(**engine_args) -> Session: + try: + engine = create_engine(Config.db_string(), **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 '{Config.db_string()}'") + return sql_session + + +def main(): + args = arg_parser.parse_args() + Config.load_configuration(vars(args)) + + if Config['logging.debug']: + logging.basicConfig(encoding='utf-8', level=logging.DEBUG) + else: + logging.basicConfig(encoding='utf-8', level=logging.INFO) + + if args.version: + _print_version() + exit(0) + + if args.print_config: + print(f'Configuration:\n{pformat(vars(args))}') + exit(0) + + if args.command == 'cli': + sql_session = _connect_to_database(echo=Config['logging.debug_sql']) + WorblehatCli.run_with_safe_exit_wrapper(sql_session) + exit(0) + + if args.command == 'flask-dev': + flask_dev_main() + exit(0) + + if args.command == 'flask-prod': + if Config['logging.debug'] or Config['logging.debug_sql']: + logging.warn('Debug mode is enabled for the production server. This is not recommended.') + flask_prod_main() + exit(0) + + print(arg_parser.format_help()) \ No newline at end of file diff --git a/worblehat/services/__init__.py b/worblehat/services/__init__.py index e69de29..0decc6e 100644 --- a/worblehat/services/__init__.py +++ b/worblehat/services/__init__.py @@ -0,0 +1,8 @@ +from .argument_parser import arg_parser +from .bookcase_item import ( + create_bookcase_item_from_isbn, + is_valid_isbn, +) +from .config import Config +from .email import send_email +from .seed_test_data import seed_data \ No newline at end of file diff --git a/worblehat/services/argument_parser.py b/worblehat/services/argument_parser.py index daa58fa..529ee51 100644 --- a/worblehat/services/argument_parser.py +++ b/worblehat/services/argument_parser.py @@ -1,12 +1,5 @@ from argparse import ArgumentParser -from os import path from pathlib import Path -from pprint import pformat - -def _print_version() -> None: - from worblehat import __version__ - print(f'Worblehat version {__version__}') - def _is_valid_file(parser: ArgumentParser, arg: str) -> Path: path = Path(arg) @@ -16,47 +9,41 @@ def _is_valid_file(parser: ArgumentParser, arg: str) -> Path: return path -def parse_args() -> dict[str, any]: - parser = ArgumentParser( - description = 'Worblehat library management system', - ) +arg_parser = ArgumentParser( + description = 'Worblehat library management system', +) - parser.add_argument( - '--verbose', - action = 'store_true', - help = 'Enable verbose mode', - ) - parser.add_argument( - '--verbose-sql', - action = 'store_true', - help = 'Enable verbose SQL mode', - ) - parser.add_argument( - '--version', - action = 'store_true', - help = 'Print version and exit', - ) - parser.add_argument( - '--config', - type=lambda x: _is_valid_file(parser, x), - help = 'Path to config file', - dest = 'config_file', - metavar = 'FILE', - ) - parser.add_argument( - '--print-config', - action = 'store_true', - help = 'Print configuration and quit', - ) +subparsers = arg_parser.add_subparsers(dest='command') +subparsers.add_parser( + 'cli', + help = 'Start the command line interface', +) +subparsers.add_parser( + 'flask-dev', + help = 'Start the web interface in development mode', +) +subparsers.add_parser( + 'flask-prod', + help = 'Start the web interface in production mode', +) - args = parser.parse_args() - - if args.version: - _print_version() - exit(0) - - if args.print_config: - print(f'Configuration:\n{pformat(vars(args))}') - exit(0) - - return vars(args) +arg_parser.add_argument( + '-V', + '--version', + action = 'store_true', + help = 'Print version and exit', +) +arg_parser.add_argument( + '-c', + '--config', + type=lambda x: _is_valid_file(arg_parser, x), + help = 'Path to config file', + dest = 'config_file', + metavar = 'FILE', +) +arg_parser.add_argument( + '-p', + '--print-config', + action = 'store_true', + help = 'Print configuration and quit', +) diff --git a/worblehat/services/config.py b/worblehat/services/config.py index 77395f4..e2f4206 100644 --- a/worblehat/services/config.py +++ b/worblehat/services/config.py @@ -20,6 +20,14 @@ class Config: raise AttributeError(f'No such attribute: {name}') return __config + @staticmethod + def read_password(password_field: str) -> str: + if Path(password_field).is_file(): + with open(password_field, 'r') as f: + return f.read() + else: + return password_field + @classmethod def _locate_configuration_file(cls) -> Path | None: @@ -56,7 +64,7 @@ class Config: hostname = db_config.get('hostname') port = db_config.get('port') username = db_config.get('username') - password = db_config.get('password') + password = cls.read_password(db_config.get('password')) database = db_config.get('database') return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}" else: