Move all functionality under a single `worblehat` command

adriangl/metadata-fetcher
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/
[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

View File

@ -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} = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,8 @@
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 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',
)

View File

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