Skip to content

Commit

Permalink
Restructure prepare_server to work similar to make_app
Browse files Browse the repository at this point in the history
Also, modularize, add comments, load config earlier.
  • Loading branch information
lukasjuhrich committed May 20, 2024
1 parent 78959ca commit f51c436
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 78 deletions.
2 changes: 2 additions & 0 deletions docker-compose.base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 46 additions & 75 deletions scripts/server_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
5 changes: 3 additions & 2 deletions web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down

0 comments on commit f51c436

Please sign in to comment.