main.py 5.27 KB
#!/usr/bin/env python3

'''
Start application and web server
'''


# python standard library
import argparse
import logging
import logging.config
import os
import ssl
import sys

# this project
from .app import App, AppException
from .serve import run_webserver
from .tools import load_yaml
from . import APP_NAME, APP_VERSION

# ----------------------------------------------------------------------------
def parse_cmdline_arguments() -> argparse.Namespace:
    '''
    Get command line arguments
    '''
    parser = argparse.ArgumentParser(
        description='Server for online tests. Enrolled students and tests '
        'have to be previously configured. Please read the documentation '
        'included with this software before running the server.')
    parser.add_argument('testfile',
                        type=str,
                        help='tests in YAML format')
    parser.add_argument('--allow-all',
                        action='store_true',
                        help='Allow all students to login immediately')
    parser.add_argument('--allow-list',
                        type=str,
                        help='File with list of students to allow immediately')
    parser.add_argument('--debug',
                        action='store_true',
                        help='Enable debug messages')
    parser.add_argument('--review',
                        action='store_true',
                        help='Review mode: doesn\'t generate test')
    parser.add_argument('--correct',
                        action='store_true',
                        help='Correct test and update JSON files and database')
    parser.add_argument('--port',
                        type=int,
                        default=8443,
                        help='port for the HTTPS server (default: 8443)')
    parser.add_argument('--version',
                        action='version',
                        version=f'{APP_VERSION} - python {sys.version}',
                        help='Show version information and exit')
    return parser.parse_args()

# ----------------------------------------------------------------------------
def get_logger_config(debug=False) -> dict:
    '''
    Load logger configuration from ~/.config directory if exists,
    otherwise set default paramenters.
    '''

    file = 'logger-debug.yaml' if debug else 'logger.yaml'
    path = os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config/'))
    try:
        return load_yaml(os.path.join(path, APP_NAME, file))
    except OSError:
        print('Using default logger configuration...')

    if debug:
        level = 'DEBUG'
        fmt = '%(asctime)s %(levelname)-8s %(module)-12s%(lineno)4d| %(message)s'
        dateformat = ''
    else:
        level = 'INFO'
        fmt = '%(asctime)s| %(levelname)-8s| %(message)s'
        dateformat = '%Y-%m-%d %H:%M:%S'
    modules = ['main', 'serve', 'app', 'models', 'questions', 'test',
               'testfactory', 'tools']
    logger = {'handlers': ['default'], 'level': level, 'propagate': False}
    return {
            'version': 1,
            'formatters': {
                'standard': {
                    'format': fmt,
                    'datefmt': dateformat,
                    },
                },
            'handlers': {
                'default': {
                    'level': level,
                    'class': 'logging.StreamHandler',
                    'formatter': 'standard',
                    'stream': 'ext://sys.stdout',
                    },
                },
            'loggers': {f'{APP_NAME}.{module}': logger for module in modules}
        }

# ----------------------------------------------------------------------------
def main() -> None:
    '''
    Tornado web server
    '''
    args = parse_cmdline_arguments()

    # --- Setup logging ------------------------------------------------------
    logging.config.dictConfig(get_logger_config(args.debug))
    logger = logging.getLogger(__name__)

    logger.info('================== Start Logging ==================')

    # --- start application --------------------------------------------------
    config = {
        'testfile': args.testfile,
        'allow_all': args.allow_all,
        'allow_list': args.allow_list,
        'debug': args.debug,
        'review': args.review,
        'correct': args.correct,
        }

    try:
        app = App(config)
    except AppException:
        logger.critical('Failed to start application!')
        sys.exit(1)

    # --- get SSL certificates -----------------------------------------------
    if 'XDG_DATA_HOME' in os.environ:
        certs_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'certs')
    else:
        certs_dir = os.path.expanduser('~/.local/share/certs')

    ssl_opt = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    try:
        ssl_opt.load_cert_chain(os.path.join(certs_dir, 'cert.pem'),
                                os.path.join(certs_dir, 'privkey.pem'))
    except FileNotFoundError:
        logger.critical('SSL certificates missing in %s', certs_dir)
        sys.exit(1)

    # --- run webserver ------------------------------------------------------
    run_webserver(app=app, ssl_opt=ssl_opt, port=args.port, debug=args.debug)


# ----------------------------------------------------------------------------
if __name__ == "__main__":
    main()