From 1f734486117453b2dc7a15af9ed8fe01675d28d5 Mon Sep 17 00:00:00 2001 From: Miguel BarĂ£o Date: Sun, 14 Jul 2019 13:25:06 +0100 Subject: [PATCH] split serve.py in two files: - main.py to setupt and start the application - serve.py to handle requests --- aprendizations/main.py | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ aprendizations/serve.py | 242 +++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- setup.py | 4 ++-- 3 files changed, 242 insertions(+), 235 deletions(-) create mode 100644 aprendizations/main.py diff --git a/aprendizations/main.py b/aprendizations/main.py new file mode 100644 index 0000000..5a415c2 --- /dev/null +++ b/aprendizations/main.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +# python standard library +import argparse +import logging +from os import environ, path +import signal +import ssl +import sys + +# third party libraries +import tornado + +# this project +from .learnapp import LearnApp, DatabaseUnusableException +from .serve import WebApplication +from .tools import load_yaml +from . import APP_NAME, APP_VERSION + + +# ---------------------------------------------------------------------------- +# Signal handler to catch Ctrl-C and abort server +# ---------------------------------------------------------------------------- +def signal_handler(signal, frame): + r = input(' --> Stop webserver? (yes/no) ').lower() + if r == 'yes': + tornado.ioloop.IOLoop.current().stop() + logging.critical('Webserver stopped.') + sys.exit(0) + else: + logging.info('Abort canceled...') + + +# ---------------------------------------------------------------------------- +def parse_cmdline_arguments(): + argparser = argparse.ArgumentParser( + description='Server for online learning. Students and topics ' + 'have to be previously configured. Please read the documentation ' + 'included with this software before running the server.' + ) + + argparser.add_argument( + 'conffile', type=str, nargs='*', + help='Topics configuration file in YAML format.' + ) + + argparser.add_argument( + '--prefix', type=str, default='.', + help='Path where the topic directories can be found (default: .)' + ) + + argparser.add_argument( + '--port', type=int, default=8443, + help='Port to be used by the HTTPS server (default: 8443)' + ) + + argparser.add_argument( + '--db', type=str, default='students.db', + help='SQLite3 database file (default: students.db)' + ) + + argparser.add_argument( + '--check', action='store_true', + help='Sanity check questions (can take awhile)' + ) + + argparser.add_argument( + '--debug', action='store_true', + help='Enable debug mode' + ) + + argparser.add_argument( + '--version', action='store_true', + help='Print version information' + ) + + return argparser.parse_args() + + +# ---------------------------------------------------------------------------- +def get_logger_config(debug=False): + if debug: + filename, level = 'logger-debug.yaml', 'DEBUG' + else: + filename, level = 'logger.yaml', '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)-10s | %(message)s', + 'datefmt': '%Y-%m-%d %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({ + APP_NAME+'.'+module: { + 'handlers': ['default'], + 'level': level, + 'propagate': False, + } for module in ['learnapp', 'models', 'factory', 'questions', + 'knowledge', 'tools']}) + + return load_yaml(config_file, default=default_config) + + +# ---------------------------------------------------------------------------- +# Tornado web server +# ---------------------------------------------------------------------------- +def main(): + # --- Commandline argument parsing + arg = parse_cmdline_arguments() + + if arg.version: + print(f'{APP_NAME} - {APP_VERSION}\nPython {sys.version}') + sys.exit(0) + + # --- Setup logging + logger_config = get_logger_config(arg.debug) + logging.config.dictConfig(logger_config) + + try: + logging.config.dictConfig(logger_config) + except Exception: + print('An error ocurred while setting up the logging system.') + sys.exit(1) + + logging.info('====================== Start Logging ======================') + + # --- start application + logging.info('Starting App...') + try: + learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db, + check=arg.check) + except DatabaseUnusableException: + logging.critical('Failed to start application.') + print('--------------------------------------------------------------') + print('Could not find a usable database. Use one of the follwing ') + print('commands to initialize: ') + print(' ') + print(' initdb-aprendizations --admin # add admin ') + print(' initdb-aprendizations -a 86 "Max Smart" # add student ') + print(' initdb-aprendizations students.csv # add many students') + print('--------------------------------------------------------------') + sys.exit(1) + except Exception: + logging.critical('Failed to start application.') + sys.exit(1) + + # --- create web application + logging.info('Starting Web App (tornado)...') + try: + webapp = WebApplication(learnapp, debug=arg.debug) + except Exception: + logging.critical('Failed to start web application.') + sys.exit(1) + + # --- 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: + logging.critical(f'SSL certificates missing in {certs_dir}') + print('--------------------------------------------------------------') + print('Certificates should be issued by a certificate authority (CA),') + print('such as https://letsencrypt.org, and then copied to: ') + print(' ') + print(f' {certs_dir:<62}') + print(' ') + print('For testing purposes a selfsigned certificate can be generated') + print('locally by running: ') + print(' ') + print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ') + print(' -out cert.pem -days 365 -nodes ') + print(' ') + print('--------------------------------------------------------------') + sys.exit(1) + + # --- create webserver + try: + httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx) + except ValueError: + logging.critical('Certificates cert.pem and privkey.pem not found') + sys.exit(1) + + try: + httpserver.listen(arg.port) + except OSError: + logging.critical(f'Cannot bind port {arg.port}. Already in use?') + sys.exit(1) + + logging.info(f'Listening on port {arg.port}.') + + # --- run webserver + signal.signal(signal.SIGINT, signal_handler) + logging.info('Webserver running. (Ctrl-C to stop)') + + try: + tornado.ioloop.IOLoop.current().start() # running... + except Exception: + logging.critical('Webserver stopped.') + tornado.ioloop.IOLoop.current().stop() + raise + + +# ---------------------------------------------------------------------------- +if __name__ == "__main__": + main() diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 915776a..cd8a59c 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -1,29 +1,20 @@ -#!/usr/bin/env python3 # python standard library -import argparse import asyncio import base64 import functools import logging.config import mimetypes -from os import path, environ -import signal -import ssl -import sys +from os import path import uuid - # third party libraries -import tornado.ioloop -import tornado.httpserver import tornado.web from tornado.escape import to_unicode # this project -from .learnapp import LearnApp, DatabaseUnusableException -from .tools import load_yaml, md_to_html -from . import APP_NAME, APP_VERSION +from .tools import md_to_html +from . import APP_NAME # ---------------------------------------------------------------------------- @@ -187,7 +178,6 @@ class RootHandler(BaseHandler): # ---------------------------------------------------------------------------- # /topic/... # Start a given topic -# FIXME should not change state... # ---------------------------------------------------------------------------- class TopicHandler(BaseHandler): @tornado.web.authenticated @@ -198,12 +188,12 @@ class TopicHandler(BaseHandler): await self.learn.start_topic(uid, topic) except KeyError: self.redirect('/') - else: - self.render('topic.html', - appname=APP_NAME, - uid=uid, - name=self.learn.get_student_name(uid), - ) + + self.render('topic.html', + appname=APP_NAME, + uid=uid, + name=self.learn.get_student_name(uid), + ) # ---------------------------------------------------------------------------- @@ -362,217 +352,3 @@ class QuestionHandler(BaseHandler): logging.error(f'Unknown action: {action}') self.write(response) - - -# ---------------------------------------------------------------------------- -# Signal handler to catch Ctrl-C and abort server -# ---------------------------------------------------------------------------- -def signal_handler(signal, frame): - r = input(' --> Stop webserver? (yes/no) ').lower() - if r == 'yes': - tornado.ioloop.IOLoop.current().stop() - logging.critical('Webserver stopped.') - sys.exit(0) - else: - logging.info('Abort canceled...') - - -# ---------------------------------------------------------------------------- -def parse_cmdline_arguments(): - argparser = argparse.ArgumentParser( - description='Server for online learning. Students and topics ' - 'have to be previously configured. Please read the documentation ' - 'included with this software before running the server.' - ) - - argparser.add_argument( - 'conffile', type=str, nargs='*', - help='Topics configuration file in YAML format.' - ) - - argparser.add_argument( - '--prefix', type=str, default='.', - help='Path where the topic directories can be found (default: .)' - ) - - argparser.add_argument( - '--port', type=int, default=8443, - help='Port to be used by the HTTPS server (default: 8443)' - ) - - argparser.add_argument( - '--db', type=str, default='students.db', - help='SQLite3 database file (default: students.db)' - ) - - argparser.add_argument( - '--check', action='store_true', - help='Sanity check questions (can take awhile)' - ) - - argparser.add_argument( - '--debug', action='store_true', - help='Enable debug mode' - ) - - argparser.add_argument( - '--version', action='store_true', - help='Print version information' - ) - - return argparser.parse_args() - - -# ---------------------------------------------------------------------------- -def get_logger_config(debug=False): - if debug: - filename, level = 'logger-debug.yaml', 'DEBUG' - else: - filename, level = 'logger.yaml', '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)-10s | %(message)s', - 'datefmt': '%Y-%m-%d %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({ - APP_NAME+'.'+module: { - 'handlers': ['default'], - 'level': level, - 'propagate': False, - } for module in ['learnapp', 'models', 'factory', 'questions', - 'knowledge', 'tools']}) - - return load_yaml(config_file, default=default_config) - - -# ---------------------------------------------------------------------------- -# Tornado web server -# ---------------------------------------------------------------------------- -def main(): - # --- Commandline argument parsing - arg = parse_cmdline_arguments() - - if arg.version: - print(APP_NAME + ' ' + APP_VERSION) - print('Python ' + sys.version) - sys.exit(0) - - # --- Setup logging - logger_config = get_logger_config(arg.debug) - logging.config.dictConfig(logger_config) - - try: - logging.config.dictConfig(logger_config) - except Exception: - print('An error ocurred while setting up the logging system.') - sys.exit(1) - - logging.info('====================== Start Logging ======================') - - # --- start application - logging.info('Starting App...') - try: - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db, - check=arg.check) - except DatabaseUnusableException: - logging.critical('Failed to start application.') - print('--------------------------------------------------------------') - print('Could not find a usable database. Use one of the follwing ') - print('commands to initialize: ') - print(' ') - print(' initdb-aprendizations --admin # add admin ') - print(' initdb-aprendizations -a 86 "Max Smart" # add student ') - print(' initdb-aprendizations students.csv # add many students') - print('--------------------------------------------------------------') - sys.exit(1) - except Exception: - logging.critical('Failed to start application.') - sys.exit(1) - - # --- create web application - logging.info('Starting Web App (tornado)...') - try: - webapp = WebApplication(learnapp, debug=arg.debug) - except Exception: - logging.critical('Failed to start web application.') - sys.exit(1) - - # --- 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: - logging.critical(f'SSL certificates missing in {certs_dir}') - print('--------------------------------------------------------------') - print('Certificates should be issued by a certificate authority (CA),') - print('such as https://letsencrypt.org, and then copied to: ') - print(' ') - print(f' {certs_dir:<62}') - print(' ') - print('For testing purposes a selfsigned certificate can be generated') - print('locally by running: ') - print(' ') - print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ') - print(' -out cert.pem -days 365 -nodes ') - print(' ') - print('--------------------------------------------------------------') - sys.exit(1) - - # --- create webserver - try: - httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx) - except ValueError: - logging.critical('Certificates cert.pem and privkey.pem not found') - sys.exit(1) - - try: - httpserver.listen(arg.port) - except OSError: - logging.critical(f'Cannot bind port {arg.port}. Already in use?') - sys.exit(1) - - logging.info(f'Listening on port {arg.port}.') - - # --- run webserver - signal.signal(signal.SIGINT, signal_handler) - logging.info('Webserver running. (Ctrl-C to stop)') - - try: - tornado.ioloop.IOLoop.current().start() # running... - except Exception: - logging.critical('Webserver stopped.') - tornado.ioloop.IOLoop.current().stop() - raise - - -# ---------------------------------------------------------------------------- -if __name__ == "__main__": - main() diff --git a/setup.py b/setup.py index 3e0a043..27480d7 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,9 @@ setup( ], entry_points={ 'console_scripts': [ - 'aprendizations = aprendizations.serve:main', + 'aprendizations = aprendizations.main:main', 'initdb-aprendizations = aprendizations.initdb:main', - 'redirect = aprendizations.redirect:main', + # 'redirect = aprendizations.redirect:main', ] }, classifiers=[ -- libgit2 0.21.2