__init__.py 5.66 KB
Newer Older
1 2
"""The main module for the application. It contains the application factory and provides access to the database."""
import logging
3
import os
4 5 6 7
import sys
from logging.handlers import RotatingFileHandler
from threading import Thread
from time import strftime
8
from typing import Type
9
import connexion
10
import flask
11
import open_alchemy
12
from connexion import FlaskApp
13
from flask import Flask, got_request_exception, request, Response, send_from_directory
14 15 16
from flask_cors import CORS
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
17
from open_alchemy import init_yaml
18 19 20 21
from mcserver.config import Config

db: SQLAlchemy = SQLAlchemy()  # session_options={"autocommit": True}
migrate: Migrate = Migrate(directory=Config.MIGRATIONS_DIRECTORY)
22 23 24 25
if not hasattr(open_alchemy.models, Config.DATABASE_TABLE_CORPUS):
    # do this _BEFORE_ you add any APIs to your application
    init_yaml(Config.API_SPEC_YAML_FILE_PATH, base=db.Model,
              models_filename=os.path.join(Config.MC_SERVER_DIRECTORY, "models_auto.py"))
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45


def apply_event_handlers(app: FlaskApp):
    """Applies event handlers to a given Flask application, such as logging after requests or teardown logic."""

    @app.app.after_request
    def after_request(response: Response) -> Response:
        """ Logs metadata for every request. """
        timestamp = strftime('[%Y-%m-%d %H:%M:%S]')
        app.app.logger.info('%s %s %s %s %s %s', timestamp, request.remote_addr, request.method, request.scheme,
                            request.full_path, response.status)
        return response

    @app.route(Config.SERVER_URI_FAVICON)
    def get_favicon():
        """Sends the favicon to browsers, which is used, e.g., in the tabs as a symbol for our application."""
        mime_type: str = 'image/vnd.microsoft.icon'
        return send_from_directory(Config.ASSETS_DIRECTORY, Config.FAVICON_FILE_NAME, mimetype=mime_type)

    app.app.teardown_appcontext(shutdown_session)
46 47 48 49 50 51 52 53 54


def create_app(cfg: Type[Config] = Config) -> Flask:
    """Create a new Flask application and configure it. Initialize the application and the database.
    Arguments:
        cfg -- the desired configuration class for the application
    """
    # use local postgres database for migrations
    if len(sys.argv) > 2 and sys.argv[2] == Config.FLASK_MIGRATE:
55
        cfg.SQLALCHEMY_DATABASE_URI = Config.DATABASE_URL_LOCAL
56
    app: Flask = init_app_common(cfg=cfg)
57 58 59 60 61 62 63 64
    from mcserver.app.services import bp as services_bp
    app.register_blueprint(services_bp)
    from mcserver.app.api import bp as api_bp
    app.register_blueprint(api_bp)
    init_logging(app, Config.LOG_PATH_MCSERVER)
    return app


65
def full_init(app: Flask, cfg: Type[Config] = Config) -> None:
66
    """ Fully initializes the application, including logging."""
67 68 69 70 71 72 73 74
    from mcserver.app.services import DatabaseService
    DatabaseService.init_db_update_info()
    DatabaseService.update_exercises(is_csm=True)
    DatabaseService.init_db_corpus()
    if not cfg.TESTING:
        from mcserver.app.services.corpusService import CorpusService
        CorpusService.init_graphannis_logging()
        start_updater(app)
75 76 77 78


def init_app_common(cfg: Type[Config] = Config, is_csm: bool = False) -> Flask:
    """ Initializes common Flask parts, e.g. CORS, configuration, database, migrations and custom corpora."""
79 80 81
    connexion_app: FlaskApp = connexion.FlaskApp(
        __name__, port=(cfg.CORPUS_STORAGE_MANAGER_PORT if is_csm else cfg.HOST_PORT),
        specification_dir=Config.MC_SERVER_DIRECTORY)
82
    connexion_app.add_api(Config.API_SPEC_YAML_FILE_PATH, arguments={'title': 'Machina Callida Backend REST API'})
83 84
    apply_event_handlers(connexion_app)
    app: Flask = connexion_app.app
85 86 87 88 89 90
    # allow CORS requests for all API routes
    CORS(app)  # , resources=r"/*"
    app.config.from_object(cfg)
    app.app_context().push()
    db.init_app(app)
    migrate.init_app(app, db)
91 92
    if is_csm or cfg.TESTING:
        db.create_all()
93 94 95
    if is_csm:
        from mcserver.app.services.databaseService import DatabaseService
        DatabaseService.init_db_alembic()
96 97 98
    from mcserver.app.services.textService import TextService
    TextService.init_proper_nouns_list()
    TextService.init_stop_words_latin()
99 100
    if is_csm:
        full_init(app, cfg)
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
    return app


def init_logging(app: Flask, log_file_path: str):
    """ Initializes logging for a given Flask application. """
    file_handler: RotatingFileHandler = RotatingFileHandler(log_file_path, maxBytes=1000 * 1000,
                                                            backupCount=3)
    log_level: int = logging.INFO
    file_handler.setLevel(log_level)
    app.logger.addHandler(file_handler)
    app.logger.setLevel(log_level)
    got_request_exception.connect(log_exception, app)


def log_exception(sender_app: Flask, exception, **extra):
    """Logs errors that occur while the Flask app is working.

    Arguments:
        sender_app -- the Flask application
        exception -- the exception to be logged
        **extra -- any additional arguments
    """
123
    sender_app.logger.exception(f"ERROR for {flask.request.url}")
124 125 126 127 128 129 130 131 132 133 134


def start_updater(app: Flask) -> Thread:
    """ Starts a new Thread for to perform updates in the background. """
    from mcserver.app.services import DatabaseService
    t = Thread(target=DatabaseService.init_updater, args=(app,))
    t.daemon = True
    t.start()
    return t


135 136 137 138 139
def shutdown_session(exception=None):
    """ Shuts down the session when the application exits. (maybe also after every request ???) """
    db.session.remove()


140 141 142
# import the models so we can access them from other parts of the app using imports from "app.models";
# this has to be at the bottom of the file
from mcserver.app import models
143
from mcserver.app import api