diff --git a/docker-compose.base.yml b/docker-compose.base.yml index b7a7813df..7f8f3e466 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -44,6 +44,8 @@ services: SWDD_HASH_KEY: key MAIL_CONFIRM_URL: "http://localhost/sipa/register/confirm/{}" PASSWORD_RESET_URL: "http://localhost/sipa/reset-password/{}" + # alternative: `scripts.server_run:prepare_server(echo=True)` + FLASK_APP: scripts.server_run:prepare_server FLASK_ENV: development dev: extends: dev-base diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index cb4126399..f810160c1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -25,7 +25,7 @@ services: - app ports: - "5000:5000" - command: ["http", "--debug"] + command: ["shell", "flask", "run", "--debug", "--host=0.0.0.0"] healthcheck: test: curl --fail http://localhost:5000 interval: 2s diff --git a/scripts/server_run.py b/scripts/server_run.py index 284239d0a..e2a6f4f0f 100755 --- a/scripts/server_run.py +++ b/scripts/server_run.py @@ -3,24 +3,24 @@ # This file is part of the Pycroft project and licensed under the terms of # the Apache License, Version 2.0. See the LICENSE file for details. -import argparse import logging import os import sys import time -from collections.abc import Callable from babel.support import Translations from flask import g, request from flask.globals import request_ctx +from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import DeferredReflection from werkzeug.middleware.profiler import ProfilerMiddleware import pycroft import web from pycroft.helpers.i18n import set_translation_lookup, get_locale from pycroft.model.session import set_scoped_session -from scripts.connection import try_create_connection, get_connection_string +from scripts.connection import get_connection_string from scripts.schema import AlembicHelper, SchemaStrategist from web import make_app, PycroftFlask @@ -30,103 +30,74 @@ ) -def prepare_server(args) -> tuple[PycroftFlask, Callable]: - """returns both the prepared app and a callback executing `app.run`""" - if args.echo: +def prepare_server(echo=False) -> PycroftFlask: + if echo: logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) logging.getLogger('sqlalchemy.pool').setLevel(logging.DEBUG) - app = make_app(args.debug) - logging.getLogger('pycroft').addHandler(default_handler) - wait_for_db: bool = args.wait_for_database - - connection_string = get_connection_string() - connection, engine = try_create_connection(connection_string, wait_for_db, app.logger, - reflections=False) - - state = AlembicHelper(connection) - if args.force_schema_create: - strategy = SchemaStrategist(state).create_then_stamp - else: - strategy = SchemaStrategist(state).determine_schema_strategy() - strategy() - - @app.before_request - def get_time(): - g.request_time = time.time() - - @app.teardown_request - def time_response(exception=None): - request_time = g.pop('request_time', None) - - if request_time: - time_taken = time.time() - request_time - if time_taken > 0.5: - app.logger.warning( - "Response took %s seconds for request %s", - time_taken, request.full_path, - ) - - connection, engine = try_create_connection(connection_string, wait_for_db, app.logger, - args.profile) + app = make_app() + # TODO rename to `default_config.toml` + app.config.from_pyfile("flask.cfg") + engine = create_engine(get_connection_string()) + with engine.connect() as connection: + _ensure_schema_up_to_date(connection) + _setup_simple_profiling(app) + DeferredReflection.prepare(engine) # TODO for what is this used? set_scoped_session( scoped_session( sessionmaker(bind=engine), scopefunc=lambda: request_ctx._get_current_object(), ) ) + _setup_translations() + if app.config.get("PROFILE", False): + app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) + return app + +def _ensure_schema_up_to_date(connection): + state = AlembicHelper(connection) + strategy = SchemaStrategist(state).determine_schema_strategy() + strategy() + + +def _setup_translations(): def lookup_translation(): ctx = request_ctx if ctx is None: return None - translations = getattr(ctx, 'pycroft_translations', None) + translations = getattr(ctx, "pycroft_translations", None) if translations is None: translations = Translations() for module in (pycroft, web): + # TODO this has a bug. The intention is to merge + # `pycroft.translations` and `web.translations`, + # but the `os.path.dirname(…)` result is nowhere used. os.path.dirname(module.__file__) - dirname = os.path.join(ctx.app.root_path, 'translations') + dirname = os.path.join(ctx.app.root_path, "translations") translations.merge(Translations.load(dirname, [get_locale()])) ctx.pycroft_translations = translations return translations set_translation_lookup(lookup_translation) - app.config.from_pyfile('flask.cfg') - if args.profile: - app.config['PROFILE'] = True - app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) - return app, lambda: app.run( - debug=args.debug, - port=args.port, - host=args.host, - threaded=True, - exclude_patterns=["**/lib/python3*"], - ) -def create_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Pycroft launcher") - parser.add_argument("--debug", action="store_true", - help="run in debug mode") - parser.add_argument("--echo", action="store_true", - help="log sqlalchemy actions") - parser.add_argument("--profile", action="store_true", - help="profile and log sql queries") - parser.add_argument("--exposed", action="store_const", const='0.0.0.0', - dest='host', help="expose server on network") - parser.add_argument("-p", "--port", action="store", - help="port to run Pycroft on", type=int, default=5000) - parser.add_argument("-w", "--wait-for-database", type=int, default=30, - help="Maximum time to wait for database to become " - "available. Use 0 to wait forever.") - parser.add_argument("--force-schema-create", action='store_true') - return parser - - -app, run_callable = prepare_server(create_parser().parse_args()) - -if __name__ == "__main__": - run_callable() +def _setup_simple_profiling(app): + @app.before_request + def get_time(): + g.request_time = time.time() + + @app.teardown_request + def time_response(exception=None): + request_time = g.pop('request_time', None) + + if request_time: + time_taken = time.time() - request_time + if time_taken > 0.5: + app.logger.warning( + "Response took %s seconds for request %s", + time_taken, request.full_path, + ) diff --git a/web/app.py b/web/app.py index ae4fd63df..beb9df791 100644 --- a/web/app.py +++ b/web/app.py @@ -83,10 +83,9 @@ def maybe_add_config_from_env(self, keys: t.Iterable[str]) -> None: self.logger.debug("Config key %s successfuly read from environment", key) -def make_app(debug: bool = False, hades_logs: bool = True) -> PycroftFlask: +def make_app(hades_logs: bool = True) -> PycroftFlask: """Create and configure the main? Flask app object""" app = PycroftFlask(__name__) - app.debug = debug # initialization code login_manager.init_app(app) @@ -104,6 +103,8 @@ def make_app(debug: bool = False, hades_logs: bool = True) -> PycroftFlask: template_filters.register_filters(app) template_tests.register_checks(app) + # NOTE: this is _only_ used for its datetime formatting capabilities, + # for translations we have our own babel interface in `pycroft.helpers.i18n.babel`! Babel(app) if hades_logs: try: