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

'''
Setup and run the application.
'''


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

# this project
from .learnapp import LearnApp, LearnException
from .serve import webserver
from . import APP_NAME, APP_VERSION


# ----------------------------------------------------------------------------
def parse_cmdline_arguments():
    '''
    Parses command line arguments. Uses the argparse package.
    '''

    parser = argparse.ArgumentParser(
        description='Webserver for interactive learning and practice. '
        'Please read the documentation included with this software before '
        'using it.'
        )

    parser.add_argument(
        'courses', type=str, nargs='?', default='courses.yaml',
        help='configuration file in YAML format.'
        )

    parser.add_argument(
        '-v', '--version', action='store_true',
        help='show version information and exit'
        )

    parser.add_argument(
        '--prefix', type=str, default='.',
        help='path where the topic directories can be found (default: .)'
        )

    parser.add_argument(
        '--port', type=int, default=8443,
        help='port for the HTTPS server (default: 8443)'
        )

    parser.add_argument(
        '--db', type=str, default='students.db',
        help='SQLite3 database file (default: students.db)'
        )

    parser.add_argument(
        '-c', '--check', action='store_true',
        help='sanity check questions (can take awhile)'
        )

    parser.add_argument(
        '--debug', action='store_true',
        help='enable debug mode'
        )

    return parser.parse_args()


# ----------------------------------------------------------------------------
def get_logger_config(debug: bool = False) -> Dict:
    '''
    Loads logger configuration in yaml format from a file, otherwise sets up a
    default configuration.
    Returns the configuration.
    '''

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


# ----------------------------------------------------------------------------
def main():
    '''
    Start application and webserver
    '''

    # --- Commandline argument parsing
    arg = parse_cmdline_arguments()

    if arg.version:
        print(f'{APP_NAME} {APP_VERSION}\nPython {sys.version}')
        return

    # --- Setup logging
    logger_config = get_logger_config(arg.debug)
    logging.config.dictConfig(logger_config)

    # setup logger for this module
    logger = logging.getLogger(__name__)
    logger.info('====================== Start Logging ======================')

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

    ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    try:
        ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),
                                path.join(certs_dir, 'privkey.pem'))
    except FileNotFoundError:
        logger.critical('SSL certificates missing in %s', certs_dir)
        print('--------------------------------------------------------------',
              'Certificates should be issued by a certificate authority (CA),',
              'such as https://letsencrypt.org.                              ',
              'For testing purposes a selfsigned certificate can be generated',
              'locally by running:                                           ',
              '                                                              ',
              '  openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\   ',
              '          -out cert.pem -days 365 -nodes                      ',
              '                                                              ',
              'Copy the cert.pem and privkey.pem files to:                   ',
              '                                                              ',
              f'  {certs_dir:<62}',
              '                                                              ',
              'See README.md for more information                            ',
              '--------------------------------------------------------------',
              sep='\n')
        sys.exit(1)
    logger.info('SSL certificates loaded')

    # --- start application --------------------------------------------------
    try:
        app = LearnApp(courses=arg.courses,
                       prefix=arg.prefix,
                       dbase=arg.db,
                       check=arg.check)
    except LearnException:
        logger.critical('Failed to start application')
        sys.exit(1)
    logger.info('LearnApp started')

    # --- run webserver forever ----------------------------------------------
    asyncio.run(webserver(app=app, 
                          ssl=ssl_ctx, 
                          port=arg.port, 
                          debug=arg.debug))
    logger.critical('Webserver stopped.')


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