Move all functionality under a single `worblehat` command

This commit is contained in:
Oystein Kristoffer Tveit 2023-05-12 02:27:26 +02:00
parent 31184dde12
commit fad38adc50
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
12 changed files with 171 additions and 118 deletions

View File

@ -1,9 +1,6 @@
# See https://flask.palletsprojects.com/en/2.3.x/config/ [logging]
[flask] debug = true
TESTING = true debug_sql = false
DEBUG = true
FLASK_ENV = 'development'
SECRET_KEY = 'change-me'
[database] [database]
# One of (sqlite, postgres) # One of (sqlite, postgres)
@ -16,5 +13,13 @@ path = './worblehat.sqlite'
host = 'localhost' host = 'localhost'
port = 5432 port = 5432
username = 'worblehat' username = 'worblehat'
password = 'change-me' password = '/var/lib/worblehat/db-password' # path or plain text
name = 'worblehat' 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

View File

@ -13,9 +13,8 @@
inherit program; inherit program;
}; };
in { in {
default = self.apps.${system}.dev; default = self.apps.${system}.worblehat;
dev = app "${self.packages.${system}.worblehat}/bin/dev"; worblehat = app "${self.packages.${system}.worblehat}/bin/worblehat";
cli = app "${self.packages.${system}.worblehat}/bin/cli";
}; };
packages.${system} = { packages.${system} = {

View File

@ -22,8 +22,7 @@ werkzeug = "^2.3.3"
poethepoet = "^0.20.0" poethepoet = "^0.20.0"
[tool.poetry.scripts] [tool.poetry.scripts]
cli = "worblehat.cli.main:main" worblehat = "worblehat.main:main"
dev = "worblehat.flaskapp.wsgi_dev:main"
[tool.poe.tasks] [tool.poe.tasks]
clean = """ clean = """

View File

@ -0,0 +1 @@
from .main import WorblehatCli

View File

@ -1,19 +1,15 @@
from textwrap import dedent from textwrap import dedent
from sqlalchemy import ( from sqlalchemy import (
create_engine,
event, event,
select, select,
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import Session
Session,
) from worblehat.services import (
from worblehat.services.bookcase_item import (
create_bookcase_item_from_isbn, create_bookcase_item_from_isbn,
is_valid_isbn, is_valid_isbn,
) )
from worblehat.services.config import Config
from worblehat.services.argument_parser import parse_args
from worblehat.models import * from worblehat.models import *
@ -29,19 +25,10 @@ from .subclis import (
# the shelves? # the shelves?
class WorblehatCli(NumberedCmd): class WorblehatCli(NumberedCmd):
sql_session: Session def __init__(self, sql_session: Session):
sql_session_dirty: bool = False
def __init__(self, args: dict[str, any] | None = None):
super().__init__() super().__init__()
self.sql_session = sql_session
try: self.sql_session_dirty = False
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)
@event.listens_for(self.sql_session, 'after_flush') @event.listens_for(self.sql_session, 'after_flush')
def mark_session_as_dirty(*_): def mark_session_as_dirty(*_):
@ -54,7 +41,23 @@ class WorblehatCli(NumberedCmd):
self.sql_session_dirty = False self.sql_session_dirty = False
self.prompt_header = None 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): def do_list_bookcases(self, _: str):
@ -222,27 +225,3 @@ class WorblehatCli(NumberedCmd):
'doc': '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()

View File

@ -13,13 +13,10 @@ from .database import db
def create_app(args: dict[str, any] | None = None): def create_app(args: dict[str, any] | None = None):
app = Flask(__name__) app = Flask(__name__)
if args is not None: app.config.update(Config['flask'])
Config.load_configuration(args) app.config.update(Config._config)
print(Config.db_string()) app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string()
app.config.update(Config['flask']) app.config['SQLALCHEMY_ECHO'] = Config['logging.debug_sql']
app.config.update(Config._config)
app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string()
app.config['SQLALCHEMY_ECHO'] = args.get('verbose_sql')
db.init_app(app) db.init_app(app)

View File

@ -1,13 +1,11 @@
from werkzeug import run_simple from werkzeug import run_simple
from worblehat.services.config import Config from worblehat.services.config import Config
from worblehat.services.argument_parser import parse_args
from .flaskapp import create_app from .flaskapp import create_app
def main(): def main():
args = parse_args() app = create_app()
app = create_app(args)
run_simple( run_simple(
hostname = 'localhost', hostname = 'localhost',
port = 5000, port = 5000,

View File

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

67
worblehat/main.py Normal file
View File

@ -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())

View File

@ -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

View File

@ -1,12 +1,5 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from os import path
from pathlib 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: def _is_valid_file(parser: ArgumentParser, arg: str) -> Path:
path = Path(arg) path = Path(arg)
@ -16,47 +9,41 @@ def _is_valid_file(parser: ArgumentParser, arg: str) -> Path:
return path return path
def parse_args() -> dict[str, any]: arg_parser = ArgumentParser(
parser = ArgumentParser( description = 'Worblehat library management system',
description = 'Worblehat library management system', )
)
parser.add_argument( subparsers = arg_parser.add_subparsers(dest='command')
'--verbose', subparsers.add_parser(
action = 'store_true', 'cli',
help = 'Enable verbose mode', help = 'Start the command line interface',
) )
parser.add_argument( subparsers.add_parser(
'--verbose-sql', 'flask-dev',
action = 'store_true', help = 'Start the web interface in development mode',
help = 'Enable verbose SQL mode', )
) subparsers.add_parser(
parser.add_argument( 'flask-prod',
'--version', help = 'Start the web interface in production mode',
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',
)
args = parser.parse_args() arg_parser.add_argument(
'-V',
if args.version: '--version',
_print_version() action = 'store_true',
exit(0) help = 'Print version and exit',
)
if args.print_config: arg_parser.add_argument(
print(f'Configuration:\n{pformat(vars(args))}') '-c',
exit(0) '--config',
type=lambda x: _is_valid_file(arg_parser, x),
return vars(args) 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',
)

View File

@ -20,6 +20,14 @@ class Config:
raise AttributeError(f'No such attribute: {name}') raise AttributeError(f'No such attribute: {name}')
return __config 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 @classmethod
def _locate_configuration_file(cls) -> Path | None: def _locate_configuration_file(cls) -> Path | None:
@ -56,7 +64,7 @@ class Config:
hostname = db_config.get('hostname') hostname = db_config.get('hostname')
port = db_config.get('port') port = db_config.get('port')
username = db_config.get('username') username = db_config.get('username')
password = db_config.get('password') password = cls.read_password(db_config.get('password'))
database = db_config.get('database') database = db_config.get('database')
return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}" return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}"
else: else: