Move all functionality under a single `worblehat` command
This commit is contained in:
parent
31184dde12
commit
fad38adc50
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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} = {
|
||||||
|
|
|
@ -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 = """
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .main import WorblehatCli
|
|
@ -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):
|
||||||
|
@ -221,28 +224,4 @@ class WorblehatCli(NumberedCmd):
|
||||||
'f': do_exit,
|
'f': do_exit,
|
||||||
'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()
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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())
|
|
@ -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
|
|
@ -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',
|
||||||
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue