Source code for web.app

import os
import typing as t

import jinja2.ext
import logging
import sentry_sdk
from flask import (
    Flask,
    current_app,
    redirect,
    render_template,
    request,
    url_for,
    g,
    make_response,
    Response,
)
from flask.typing import ResponseValue, ResponseReturnValue
from flask_babel import Babel
from flask_login import current_user, LoginManager
from jinja2 import select_autoescape
from werkzeug.datastructures import ImmutableDict
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from werkzeug.exceptions import HTTPException

from hades_logs import HadesLogs
from pycroft.helpers.i18n import gettext
from pycroft.model import session
from web import api
from web.blueprints import task
from . import template_filters, template_tests
from .blueprints import (
    facilities,
    finance,
    infrastructure,
    login,
    properties,
    user,
    host,
    health,
    mpskclient,
)

from .blueprints.login import login_manager
from .commands import register_commands
from .templates import page_resources


[docs] class PycroftFlask(Flask): """ Extend the Flask class to set Jinja options. """ jinja_options = ImmutableDict( Flask.jinja_options, extensions=[ jinja2.ext.do, jinja2.ext.loopcontrols, ], autoescape=select_autoescape(), undefined=jinja2.StrictUndefined, ) login_manager: LoginManager @t.override def __init__(self, *a: t.Any, **kw: t.Any) -> None: super().__init__(*a, **kw) # config keys to support: self.maybe_add_config_from_env([ 'PYCROFT_API_KEY', 'HADES_CELERY_APP_NAME', 'HADES_BROKER_URI', 'HADES_RESULT_BACKEND_URI', 'HADES_TIMEOUT', 'HADES_ROUTING_KEY', ])
[docs] def maybe_add_config_from_env(self, keys: t.Iterable[str]) -> None: """Write keys from the environment to the app's config If a key does not exist in the environment, it will just be skipped. """ for key in keys: try: self.config[key] = os.environ[key] except KeyError: self.logger.debug("Config key %s not present in environment, skipping", key) continue else: self.logger.debug("Config key %s successfuly read from environment", key)
[docs] def make_app(hades_logs: bool = True) -> PycroftFlask: """Create and configure the main? Flask app object""" app = PycroftFlask(__name__) # initialization code login_manager.init_app(app) app.register_blueprint(user.bp, url_prefix="/user") app.register_blueprint(facilities.bp, url_prefix="/facilities") app.register_blueprint(infrastructure.bp, url_prefix="/infrastructure") app.register_blueprint(properties.bp, url_prefix="/properties") app.register_blueprint(finance.bp, url_prefix="/finance") app.register_blueprint(host.bp, url_prefix="/host") app.register_blueprint(task.bp, url_prefix="/task") app.register_blueprint(login.bp) app.register_blueprint(api.bp, url_prefix="/api/v0") app.register_blueprint(health.bp, url_prefix="/health") app.register_blueprint(mpskclient.bp, url_prefix="/wifi-mpsk") 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: HadesLogs(app) except KeyError as e: app.logger.info("HadesLogs configuration incomplete, skipping.") app.logger.info("Original error: %s", str(e)) else: app.logger.info("HadesLogs configuration disabled. Skipping.") page_resources.init_app(app) user.nav.register_on(app) finance.nav.register_on(app) facilities.nav.register_on(app) infrastructure.nav.register_on(app) task.nav.register_on(app) properties.nav.register_on(app) @app.errorhandler(403) @app.errorhandler(404) @app.errorhandler(500) def errorpage(e: Exception) -> ResponseReturnValue: """Handle errors according to their error code :param e: The error from the errorhandler """ code = getattr(e, "code", 500) if code == 500: message = str(e) elif code == 403: message = gettext("You are not allowed to access this page.") elif code == 404: message = gettext("Page not found.") else: raise AssertionError() return render_template('error.html', error=message), code @app.route('/') def redirect_to_index() -> ResponseValue: return redirect(url_for('user.overview')) @app.route('/debug-sentry') def debug_sentry() -> t.NoReturn: app.logger.warning("Someone used the debug-sentry endpoint! Also, this is a test warning.") app.logger.info("An info log for inbetween") app.logger.error("Someone used the debug-sentry endpoint! Also, this is a test error.", extra={'pi': 3.141}) div_by_zero = 1 / 0 # noqa assert False # noqa: B011 @app.teardown_request def shutdown_session(exception: BaseException | None = None) -> None: if app.testing: # things are not necessarily committed here, # so `remove` would result in a `ROLLBACK TO SAVEPOINT` to a pre-setup state. return session.Session.remove() @app.before_request def require_login() -> ResponseReturnValue | None: """Request a login for every page except the login blueprint and the static folder. Blueprint "None" is needed for "/static/*" GET requests. """ if current_user.is_anonymous and request.blueprint not in ( "login", "api", "health", None, ): lm = t.cast(LoginManager, current_app.login_manager) # type: ignore[attr-defined] return lm.unauthorized() return None if app.debug: register_pyinstrument(app) register_commands(app) return app
[docs] def register_pyinstrument(app: Flask) -> None: try: from pyinstrument import Profiler except ImportError: app.logger.info("in debug mode, but pyinstrument not installed.") return @app.before_request def before_request() -> None: if "profile" in request.args: g.profiler = Profiler() g.profiler.start() @app.after_request def after_request(response: Response) -> Response: if not hasattr(g, "profiler"): return response g.profiler.stop() output_html = g.profiler.output_html() return make_response(output_html)
IGNORED_EXCEPTION_TYPES = (HTTPException,) if dsn := os.getenv('PYCROFT_SENTRY_DSN'): def before_send[_TE](event: _TE, hint: dict[str, t.Any]) -> _TE | None: if 'exc_info' in hint: exc_type, exc_value, _tb = hint['exc_info'] if isinstance(exc_value, IGNORED_EXCEPTION_TYPES): return None return event logging_integration = LoggingIntegration( level=logging.INFO, # INFO / WARN create breadcrumbs, just as SQL queries event_level=logging.ERROR, # errors and above create breadcrumbs ) sentry_sdk.init( dsn=dsn, integrations=[FlaskIntegration(), logging_integration], traces_sample_rate=1.0, before_send=before_send )