import logging import os import re import sys from logging.config import dictConfig from pathlib import Path from sys import argv from typing import Optional from otree import __version__ # adapted from uvicorn LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { # we could use colorama to get coloring for windows. otherwise we get: # INFO: # "()": "uvicorn.logging.DefaultFormatter", "fmt": "%(levelprefix)s %(message)s", "use_colors": None, }, "access": { "()": "uvicorn.logging.AccessFormatter", "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501 }, }, "handlers": { "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr", }, "access": { "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", }, }, "loggers": { "uvicorn": {"handlers": ["default"], "level": "INFO"}, "uvicorn.error": {"level": "INFO", "propagate": False}, "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, }, 'root': {'handlers': ['default'], 'level': logging.INFO,}, } dictConfig(LOGGING_CONFIG) logger = logging.getLogger(__name__) print_function = print COMMAND_ALIASES = dict( test='bots', runprodserver='prodserver', webandworkers='prodserver1of2', runprodserver1of2='prodserver1of2', runprodserver2of2='prodserver2of2', timeoutworker='prodserver2of2', version='--version', help='--help', ) def execute_from_command_line(*args, **kwargs): if len(argv) == 1: argv.append('help') cmd = argv[1] cmd = COMMAND_ALIASES.get(cmd, cmd) if cmd == '--version': print_function(__version__) return if cmd == '--help': print_function(MAIN_HELP_TEXT) return # [does the below caveat still apply without django?] # need to set env var rather than setting otree.common.USE_TIMEOUT_WORKER because # that module cannot be loaded yet. if cmd in ['prodserver', 'prodserver1of2']: os.environ['USE_TIMEOUT_WORKER'] = '1' if cmd in [ 'startproject', 'version', '--version', 'unzip', 'zip', 'zipserver', 'devserver', 'update_my_code', 'remove_self', 'remove_self_finalize', ]: # skip full setup. pass else: if cmd in ['devserver_inner', 'bots']: os.environ['OTREE_IN_MEMORY'] = '1' setup() warning = check_update_needed(Path('.').resolve().joinpath('requirements.txt')) if warning: logger.warning(warning) from otree.cli.base import call_command call_command(cmd, *argv[2:]) MSG_V5_WELCOME = """ Welcome to oTree Lite! To ensure this project runs properly on oTree Lite: 1) Run "otree update_my_code" and fix any warning messages. 2) Delete 'db.sqlite3' and the folder '__temp_migrations'. oTree Lite is generally compatible with oTree 3.x but there may be small differences. Note that it does not use Django. If you want to go back to oTree 3.x, enter: pip3 install -U "otree<5" """ def setup(): if Path('__temp_migrations').exists(): sys.exit(MSG_V5_WELCOME) from otree import settings init_i18n(settings.LANGUAGE_CODE_ISO) from otree.database import init_orm # noqa check_for_sentry() init_orm() import otree.bots.browser otree.bots.browser.browser_bot_worker = otree.bots.browser.BotWorker() def init_i18n(LANGUAGE_CODE_ISO): os.environ['LANGUAGE'] = LANGUAGE_CODE_ISO import gettext # because the files are called django.mo gettext.textdomain('django') gettext.bindtextdomain('django', localedir=str(Path(__file__).parent / 'locale')) if Path('_locale').is_dir(): gettext.bindtextdomain('messages', localedir='_locale') gettext.textdomain('messages') def split_dotted_version(version): return [int(n) for n in version.split('.')] def check_update_needed( requirements_path: Path, current_version=__version__ ) -> Optional[str]: '''rewrote this without pkg_resources since that takes 0.4 seconds just to import''' if not requirements_path.exists(): return for line in requirements_path.read_text('utf8').splitlines(): if (not line.startswith('otree')) or ' ' in line or '\t' in line: continue for start in ['otree>=', 'otree[mturk]>=']: if line.startswith(start): return check_update_needed_line(line, current_version) def check_update_needed_line(line, current_version): """This is for zipserver mainly""" version_dotted = re.search(r'>=([\d\.]+)\b', line) if version_dotted: try: required_version = split_dotted_version(version_dotted.group(1)) installed_version = split_dotted_version(current_version) except ValueError: return if required_version > installed_version: return f'''This project requires a newer oTree version. Enter: pip3 install "{line}"''' def send_termination_notice(PORT) -> int: # expensive imports. do it in here so that we don't delay devserver_inner from urllib.request import urlopen import urllib.error try: # send data= so it makes a post request # it seems using localhost is very slow compared to 127.0.0.1 resp = urlopen(f'http://127.0.0.1:{PORT}/SaveDB', data=b'foo') return int(resp.read().decode('utf-8')) except urllib.error.URLError as exc: # - URLError may happen if the server didn't even start up yet # (if you stop it right away) pass def check_for_sentry(): SENTRY_DSN = os.environ.get('SENTRY_DSN') if SENTRY_DSN: try: import sentry_sdk except ModuleNotFoundError: sys.exit( 'For Sentry to work, you need to add sentry_sdk to your requirements.txt.' ) from sentry_sdk.utils import json_dumps from urllib.request import urlopen import urllib.error def before_send(event, hint): otree_post_url = os.getenv('OTREE_ERROR_REPORT_ENDPOINT') if otree_post_url: try: urlopen(otree_post_url, data=json_dumps(event), timeout=0.5) except urllib.error.URLError: # in case timeout is raised etc pass return event sentry_sdk.init( dsn=SENTRY_DSN, # 2018-11-24: breadcrumbs were causing memory leaks when doing queries, # especially when creating sessions, which construct hugely verbose # queries with bulk_create. # however, i could only clearly observe the difference this line makes # when testing # on a script that bulk_created thousands of non-otree models. # when testing on a live server, things are more ambiguous. # maybe just refreshing the page several times after creating a session # is enough to reset memory to reasnoable levels? # disabling also may make things faster... # in anecdotal test, 40 vs 50 seconds max_breadcrumbs=0, release=__version__, before_send=before_send, ) MAIN_HELP_TEXT = ''' Available subcommands: browser_bots create_session devserver prodserver prodserver1of2 prodserver2of2 remove_self resetdb startapp startproject test unzip update_my_code zip zipserver '''