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

'''
Main file that starts the application and the web server
'''


# python standard library
import argparse
import logging
import os
from os import environ, path
import ssl
import sys
# from typing import Any, Dict

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


# ----------------------------------------------------------------------------
def parse_cmdline_arguments():
    '''
    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,
                        # nargs='+',  # TODO
                        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('--show-ref',
                        action='store_true',
                        help='Show question references')
    parser.add_argument('--review',
                        action='store_true',
                        help='Review mode: doesn\'t generate test')
    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):
    '''
    Load logger configuration from ~/.config directory if exists,
    otherwise set default paramenters.
    '''
    if debug:
        filename = 'logger-debug.yaml'
        level = 'DEBUG'
    else:
        filename = 'logger.yaml'
        level = 'INFO'

    config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
    config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)

    default_config = {
        'version': 1,
        'formatters': {
            'standard': {
                'format': '%(asctime)s %(levelname)-8s %(message)s',
                'datefmt': '%H:%M:%S',
                },
            },
        'handlers': {
            'default': {
                'level': level,
                'class': 'logging.StreamHandler',
                'formatter': 'standard',
                'stream': 'ext://sys.stdout',
                },
            },
        'loggers': {
            '': {  # configuration for serve.py
                'handlers': ['default'],
                'level': level,
                },
            },
        }
    default_config['loggers'].update({
        f'{APP_NAME}.{module}': {
            'handlers': ['default'],
            'level': level,
            'propagate': False,
            } for module in ['app', 'models', 'factory', 'questions',
                             'test', 'tools']})

    return load_yaml(config_file, default=default_config)


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

    # --- Setup logging ------------------------------------------------------
    logging.config.dictConfig(get_logger_config(args.debug))
    logging.info('====================== Start Logging ======================')

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

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

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

    ssl_opt = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    try:
        ssl_opt.load_cert_chain(path.join(certs_dir, 'cert.pem'),
                                path.join(certs_dir, 'privkey.pem'))
    except FileNotFoundError:
        logging.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()