Commit 1f734486117453b2dc7a15af9ed8fe01675d28d5

Authored by Miguel Barão
1 parent 7b668cb1
Exists in master and in 1 other branch dev

split serve.py in two files:

- main.py to setupt and start the application
- serve.py to handle requests
aprendizations/main.py 0 → 100644
@@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +# python standard library
  4 +import argparse
  5 +import logging
  6 +from os import environ, path
  7 +import signal
  8 +import ssl
  9 +import sys
  10 +
  11 +# third party libraries
  12 +import tornado
  13 +
  14 +# this project
  15 +from .learnapp import LearnApp, DatabaseUnusableException
  16 +from .serve import WebApplication
  17 +from .tools import load_yaml
  18 +from . import APP_NAME, APP_VERSION
  19 +
  20 +
  21 +# ----------------------------------------------------------------------------
  22 +# Signal handler to catch Ctrl-C and abort server
  23 +# ----------------------------------------------------------------------------
  24 +def signal_handler(signal, frame):
  25 + r = input(' --> Stop webserver? (yes/no) ').lower()
  26 + if r == 'yes':
  27 + tornado.ioloop.IOLoop.current().stop()
  28 + logging.critical('Webserver stopped.')
  29 + sys.exit(0)
  30 + else:
  31 + logging.info('Abort canceled...')
  32 +
  33 +
  34 +# ----------------------------------------------------------------------------
  35 +def parse_cmdline_arguments():
  36 + argparser = argparse.ArgumentParser(
  37 + description='Server for online learning. Students and topics '
  38 + 'have to be previously configured. Please read the documentation '
  39 + 'included with this software before running the server.'
  40 + )
  41 +
  42 + argparser.add_argument(
  43 + 'conffile', type=str, nargs='*',
  44 + help='Topics configuration file in YAML format.'
  45 + )
  46 +
  47 + argparser.add_argument(
  48 + '--prefix', type=str, default='.',
  49 + help='Path where the topic directories can be found (default: .)'
  50 + )
  51 +
  52 + argparser.add_argument(
  53 + '--port', type=int, default=8443,
  54 + help='Port to be used by the HTTPS server (default: 8443)'
  55 + )
  56 +
  57 + argparser.add_argument(
  58 + '--db', type=str, default='students.db',
  59 + help='SQLite3 database file (default: students.db)'
  60 + )
  61 +
  62 + argparser.add_argument(
  63 + '--check', action='store_true',
  64 + help='Sanity check questions (can take awhile)'
  65 + )
  66 +
  67 + argparser.add_argument(
  68 + '--debug', action='store_true',
  69 + help='Enable debug mode'
  70 + )
  71 +
  72 + argparser.add_argument(
  73 + '--version', action='store_true',
  74 + help='Print version information'
  75 + )
  76 +
  77 + return argparser.parse_args()
  78 +
  79 +
  80 +# ----------------------------------------------------------------------------
  81 +def get_logger_config(debug=False):
  82 + if debug:
  83 + filename, level = 'logger-debug.yaml', 'DEBUG'
  84 + else:
  85 + filename, level = 'logger.yaml', 'INFO'
  86 +
  87 + config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
  88 + config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)
  89 +
  90 + default_config = {
  91 + 'version': 1,
  92 + 'formatters': {
  93 + 'standard': {
  94 + 'format': '%(asctime)s | %(levelname)-10s | %(message)s',
  95 + 'datefmt': '%Y-%m-%d %H:%M:%S',
  96 + },
  97 + },
  98 + 'handlers': {
  99 + 'default': {
  100 + 'level': level,
  101 + 'class': 'logging.StreamHandler',
  102 + 'formatter': 'standard',
  103 + 'stream': 'ext://sys.stdout',
  104 + },
  105 + },
  106 + 'loggers': {
  107 + '': { # configuration for serve.py
  108 + 'handlers': ['default'],
  109 + 'level': level,
  110 + },
  111 + },
  112 + }
  113 + default_config['loggers'].update({
  114 + APP_NAME+'.'+module: {
  115 + 'handlers': ['default'],
  116 + 'level': level,
  117 + 'propagate': False,
  118 + } for module in ['learnapp', 'models', 'factory', 'questions',
  119 + 'knowledge', 'tools']})
  120 +
  121 + return load_yaml(config_file, default=default_config)
  122 +
  123 +
  124 +# ----------------------------------------------------------------------------
  125 +# Tornado web server
  126 +# ----------------------------------------------------------------------------
  127 +def main():
  128 + # --- Commandline argument parsing
  129 + arg = parse_cmdline_arguments()
  130 +
  131 + if arg.version:
  132 + print(f'{APP_NAME} - {APP_VERSION}\nPython {sys.version}')
  133 + sys.exit(0)
  134 +
  135 + # --- Setup logging
  136 + logger_config = get_logger_config(arg.debug)
  137 + logging.config.dictConfig(logger_config)
  138 +
  139 + try:
  140 + logging.config.dictConfig(logger_config)
  141 + except Exception:
  142 + print('An error ocurred while setting up the logging system.')
  143 + sys.exit(1)
  144 +
  145 + logging.info('====================== Start Logging ======================')
  146 +
  147 + # --- start application
  148 + logging.info('Starting App...')
  149 + try:
  150 + learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,
  151 + check=arg.check)
  152 + except DatabaseUnusableException:
  153 + logging.critical('Failed to start application.')
  154 + print('--------------------------------------------------------------')
  155 + print('Could not find a usable database. Use one of the follwing ')
  156 + print('commands to initialize: ')
  157 + print(' ')
  158 + print(' initdb-aprendizations --admin # add admin ')
  159 + print(' initdb-aprendizations -a 86 "Max Smart" # add student ')
  160 + print(' initdb-aprendizations students.csv # add many students')
  161 + print('--------------------------------------------------------------')
  162 + sys.exit(1)
  163 + except Exception:
  164 + logging.critical('Failed to start application.')
  165 + sys.exit(1)
  166 +
  167 + # --- create web application
  168 + logging.info('Starting Web App (tornado)...')
  169 + try:
  170 + webapp = WebApplication(learnapp, debug=arg.debug)
  171 + except Exception:
  172 + logging.critical('Failed to start web application.')
  173 + sys.exit(1)
  174 +
  175 + # --- get SSL certificates
  176 + if 'XDG_DATA_HOME' in environ:
  177 + certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')
  178 + else:
  179 + certs_dir = path.expanduser('~/.local/share/certs')
  180 +
  181 + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  182 + try:
  183 + ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),
  184 + path.join(certs_dir, 'privkey.pem'))
  185 + except FileNotFoundError:
  186 + logging.critical(f'SSL certificates missing in {certs_dir}')
  187 + print('--------------------------------------------------------------')
  188 + print('Certificates should be issued by a certificate authority (CA),')
  189 + print('such as https://letsencrypt.org, and then copied to: ')
  190 + print(' ')
  191 + print(f' {certs_dir:<62}')
  192 + print(' ')
  193 + print('For testing purposes a selfsigned certificate can be generated')
  194 + print('locally by running: ')
  195 + print(' ')
  196 + print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ')
  197 + print(' -out cert.pem -days 365 -nodes ')
  198 + print(' ')
  199 + print('--------------------------------------------------------------')
  200 + sys.exit(1)
  201 +
  202 + # --- create webserver
  203 + try:
  204 + httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)
  205 + except ValueError:
  206 + logging.critical('Certificates cert.pem and privkey.pem not found')
  207 + sys.exit(1)
  208 +
  209 + try:
  210 + httpserver.listen(arg.port)
  211 + except OSError:
  212 + logging.critical(f'Cannot bind port {arg.port}. Already in use?')
  213 + sys.exit(1)
  214 +
  215 + logging.info(f'Listening on port {arg.port}.')
  216 +
  217 + # --- run webserver
  218 + signal.signal(signal.SIGINT, signal_handler)
  219 + logging.info('Webserver running. (Ctrl-C to stop)')
  220 +
  221 + try:
  222 + tornado.ioloop.IOLoop.current().start() # running...
  223 + except Exception:
  224 + logging.critical('Webserver stopped.')
  225 + tornado.ioloop.IOLoop.current().stop()
  226 + raise
  227 +
  228 +
  229 +# ----------------------------------------------------------------------------
  230 +if __name__ == "__main__":
  231 + main()
aprendizations/serve.py
1 -#!/usr/bin/env python3  
2 1
3 # python standard library 2 # python standard library
4 -import argparse  
5 import asyncio 3 import asyncio
6 import base64 4 import base64
7 import functools 5 import functools
8 import logging.config 6 import logging.config
9 import mimetypes 7 import mimetypes
10 -from os import path, environ  
11 -import signal  
12 -import ssl  
13 -import sys 8 +from os import path
14 import uuid 9 import uuid
15 10
16 -  
17 # third party libraries 11 # third party libraries
18 -import tornado.ioloop  
19 -import tornado.httpserver  
20 import tornado.web 12 import tornado.web
21 from tornado.escape import to_unicode 13 from tornado.escape import to_unicode
22 14
23 # this project 15 # this project
24 -from .learnapp import LearnApp, DatabaseUnusableException  
25 -from .tools import load_yaml, md_to_html  
26 -from . import APP_NAME, APP_VERSION 16 +from .tools import md_to_html
  17 +from . import APP_NAME
27 18
28 19
29 # ---------------------------------------------------------------------------- 20 # ----------------------------------------------------------------------------
@@ -187,7 +178,6 @@ class RootHandler(BaseHandler): @@ -187,7 +178,6 @@ class RootHandler(BaseHandler):
187 # ---------------------------------------------------------------------------- 178 # ----------------------------------------------------------------------------
188 # /topic/... 179 # /topic/...
189 # Start a given topic 180 # Start a given topic
190 -# FIXME should not change state...  
191 # ---------------------------------------------------------------------------- 181 # ----------------------------------------------------------------------------
192 class TopicHandler(BaseHandler): 182 class TopicHandler(BaseHandler):
193 @tornado.web.authenticated 183 @tornado.web.authenticated
@@ -198,12 +188,12 @@ class TopicHandler(BaseHandler): @@ -198,12 +188,12 @@ class TopicHandler(BaseHandler):
198 await self.learn.start_topic(uid, topic) 188 await self.learn.start_topic(uid, topic)
199 except KeyError: 189 except KeyError:
200 self.redirect('/') 190 self.redirect('/')
201 - else:  
202 - self.render('topic.html',  
203 - appname=APP_NAME,  
204 - uid=uid,  
205 - name=self.learn.get_student_name(uid),  
206 - ) 191 +
  192 + self.render('topic.html',
  193 + appname=APP_NAME,
  194 + uid=uid,
  195 + name=self.learn.get_student_name(uid),
  196 + )
207 197
208 198
209 # ---------------------------------------------------------------------------- 199 # ----------------------------------------------------------------------------
@@ -362,217 +352,3 @@ class QuestionHandler(BaseHandler): @@ -362,217 +352,3 @@ class QuestionHandler(BaseHandler):
362 logging.error(f'Unknown action: {action}') 352 logging.error(f'Unknown action: {action}')
363 353
364 self.write(response) 354 self.write(response)
365 -  
366 -  
367 -# ----------------------------------------------------------------------------  
368 -# Signal handler to catch Ctrl-C and abort server  
369 -# ----------------------------------------------------------------------------  
370 -def signal_handler(signal, frame):  
371 - r = input(' --> Stop webserver? (yes/no) ').lower()  
372 - if r == 'yes':  
373 - tornado.ioloop.IOLoop.current().stop()  
374 - logging.critical('Webserver stopped.')  
375 - sys.exit(0)  
376 - else:  
377 - logging.info('Abort canceled...')  
378 -  
379 -  
380 -# ----------------------------------------------------------------------------  
381 -def parse_cmdline_arguments():  
382 - argparser = argparse.ArgumentParser(  
383 - description='Server for online learning. Students and topics '  
384 - 'have to be previously configured. Please read the documentation '  
385 - 'included with this software before running the server.'  
386 - )  
387 -  
388 - argparser.add_argument(  
389 - 'conffile', type=str, nargs='*',  
390 - help='Topics configuration file in YAML format.'  
391 - )  
392 -  
393 - argparser.add_argument(  
394 - '--prefix', type=str, default='.',  
395 - help='Path where the topic directories can be found (default: .)'  
396 - )  
397 -  
398 - argparser.add_argument(  
399 - '--port', type=int, default=8443,  
400 - help='Port to be used by the HTTPS server (default: 8443)'  
401 - )  
402 -  
403 - argparser.add_argument(  
404 - '--db', type=str, default='students.db',  
405 - help='SQLite3 database file (default: students.db)'  
406 - )  
407 -  
408 - argparser.add_argument(  
409 - '--check', action='store_true',  
410 - help='Sanity check questions (can take awhile)'  
411 - )  
412 -  
413 - argparser.add_argument(  
414 - '--debug', action='store_true',  
415 - help='Enable debug mode'  
416 - )  
417 -  
418 - argparser.add_argument(  
419 - '--version', action='store_true',  
420 - help='Print version information'  
421 - )  
422 -  
423 - return argparser.parse_args()  
424 -  
425 -  
426 -# ----------------------------------------------------------------------------  
427 -def get_logger_config(debug=False):  
428 - if debug:  
429 - filename, level = 'logger-debug.yaml', 'DEBUG'  
430 - else:  
431 - filename, level = 'logger.yaml', 'INFO'  
432 -  
433 - config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')  
434 - config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)  
435 -  
436 - default_config = {  
437 - 'version': 1,  
438 - 'formatters': {  
439 - 'standard': {  
440 - 'format': '%(asctime)s | %(levelname)-10s | %(message)s',  
441 - 'datefmt': '%Y-%m-%d %H:%M:%S',  
442 - },  
443 - },  
444 - 'handlers': {  
445 - 'default': {  
446 - 'level': level,  
447 - 'class': 'logging.StreamHandler',  
448 - 'formatter': 'standard',  
449 - 'stream': 'ext://sys.stdout',  
450 - },  
451 - },  
452 - 'loggers': {  
453 - '': { # configuration for serve.py  
454 - 'handlers': ['default'],  
455 - 'level': level,  
456 - },  
457 - },  
458 - }  
459 - default_config['loggers'].update({  
460 - APP_NAME+'.'+module: {  
461 - 'handlers': ['default'],  
462 - 'level': level,  
463 - 'propagate': False,  
464 - } for module in ['learnapp', 'models', 'factory', 'questions',  
465 - 'knowledge', 'tools']})  
466 -  
467 - return load_yaml(config_file, default=default_config)  
468 -  
469 -  
470 -# ----------------------------------------------------------------------------  
471 -# Tornado web server  
472 -# ----------------------------------------------------------------------------  
473 -def main():  
474 - # --- Commandline argument parsing  
475 - arg = parse_cmdline_arguments()  
476 -  
477 - if arg.version:  
478 - print(APP_NAME + ' ' + APP_VERSION)  
479 - print('Python ' + sys.version)  
480 - sys.exit(0)  
481 -  
482 - # --- Setup logging  
483 - logger_config = get_logger_config(arg.debug)  
484 - logging.config.dictConfig(logger_config)  
485 -  
486 - try:  
487 - logging.config.dictConfig(logger_config)  
488 - except Exception:  
489 - print('An error ocurred while setting up the logging system.')  
490 - sys.exit(1)  
491 -  
492 - logging.info('====================== Start Logging ======================')  
493 -  
494 - # --- start application  
495 - logging.info('Starting App...')  
496 - try:  
497 - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,  
498 - check=arg.check)  
499 - except DatabaseUnusableException:  
500 - logging.critical('Failed to start application.')  
501 - print('--------------------------------------------------------------')  
502 - print('Could not find a usable database. Use one of the follwing ')  
503 - print('commands to initialize: ')  
504 - print(' ')  
505 - print(' initdb-aprendizations --admin # add admin ')  
506 - print(' initdb-aprendizations -a 86 "Max Smart" # add student ')  
507 - print(' initdb-aprendizations students.csv # add many students')  
508 - print('--------------------------------------------------------------')  
509 - sys.exit(1)  
510 - except Exception:  
511 - logging.critical('Failed to start application.')  
512 - sys.exit(1)  
513 -  
514 - # --- create web application  
515 - logging.info('Starting Web App (tornado)...')  
516 - try:  
517 - webapp = WebApplication(learnapp, debug=arg.debug)  
518 - except Exception:  
519 - logging.critical('Failed to start web application.')  
520 - sys.exit(1)  
521 -  
522 - # --- get SSL certificates  
523 - if 'XDG_DATA_HOME' in environ:  
524 - certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')  
525 - else:  
526 - certs_dir = path.expanduser('~/.local/share/certs')  
527 -  
528 - ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)  
529 - try:  
530 - ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),  
531 - path.join(certs_dir, 'privkey.pem'))  
532 - except FileNotFoundError:  
533 - logging.critical(f'SSL certificates missing in {certs_dir}')  
534 - print('--------------------------------------------------------------')  
535 - print('Certificates should be issued by a certificate authority (CA),')  
536 - print('such as https://letsencrypt.org, and then copied to: ')  
537 - print(' ')  
538 - print(f' {certs_dir:<62}')  
539 - print(' ')  
540 - print('For testing purposes a selfsigned certificate can be generated')  
541 - print('locally by running: ')  
542 - print(' ')  
543 - print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ')  
544 - print(' -out cert.pem -days 365 -nodes ')  
545 - print(' ')  
546 - print('--------------------------------------------------------------')  
547 - sys.exit(1)  
548 -  
549 - # --- create webserver  
550 - try:  
551 - httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)  
552 - except ValueError:  
553 - logging.critical('Certificates cert.pem and privkey.pem not found')  
554 - sys.exit(1)  
555 -  
556 - try:  
557 - httpserver.listen(arg.port)  
558 - except OSError:  
559 - logging.critical(f'Cannot bind port {arg.port}. Already in use?')  
560 - sys.exit(1)  
561 -  
562 - logging.info(f'Listening on port {arg.port}.')  
563 -  
564 - # --- run webserver  
565 - signal.signal(signal.SIGINT, signal_handler)  
566 - logging.info('Webserver running. (Ctrl-C to stop)')  
567 -  
568 - try:  
569 - tornado.ioloop.IOLoop.current().start() # running...  
570 - except Exception:  
571 - logging.critical('Webserver stopped.')  
572 - tornado.ioloop.IOLoop.current().stop()  
573 - raise  
574 -  
575 -  
576 -# ----------------------------------------------------------------------------  
577 -if __name__ == "__main__":  
578 - main()  
@@ -25,9 +25,9 @@ setup( @@ -25,9 +25,9 @@ setup(
25 ], 25 ],
26 entry_points={ 26 entry_points={
27 'console_scripts': [ 27 'console_scripts': [
28 - 'aprendizations = aprendizations.serve:main', 28 + 'aprendizations = aprendizations.main:main',
29 'initdb-aprendizations = aprendizations.initdb:main', 29 'initdb-aprendizations = aprendizations.initdb:main',
30 - 'redirect = aprendizations.redirect:main', 30 + # 'redirect = aprendizations.redirect:main',
31 ] 31 ]
32 }, 32 },
33 classifiers=[ 33 classifiers=[