import logging import os import sys from logging.config import dictConfig from pathlib import Path from sys import argv 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}, # otherwise we get 'connection open', 'connection closed' on each page "websockets.server": {"propagate": False, "level": "WARNING"}, }, '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 'devserver' in cmd: os.environ['USE_TEMPLATE_RELOADER'] = '1' if cmd in [ 'startproject', 'version', '--version', 'unzip', 'zip', 'zipserver', 'devserver', ]: # skip full setup. pass else: if cmd in ['devserver_inner', 'bots']: os.environ['OTREE_IN_MEMORY'] = '1' # note-to-self # 2023-07-11: seems that bots should start from scratch, # so that when the bot runner exports CSV, it should only have bot data. # however, i got a sqlalchemy error that suggests I need to do something # with DBWrapper and new_connection. Look into it later. # os.environ['OTREE_EPHEMERAL'] = '1' setup() from otree.cli.base import call_command call_command(cmd, *argv[2:]) def setup(): from otree import settings init_i18n(settings.LANGUAGE_CODE_ISO) # Import CURRENCY_UNIT class if defined (delayed to avoid circular imports) if settings.CURRENCY_UNIT_PATH: import importlib import sys from otree.decimal import DecimalUnit try: module_path, class_name = settings.CURRENCY_UNIT_PATH.rsplit('.', 1) module = importlib.import_module(module_path) currency_unit_class = getattr(module, class_name) # Validate it's a DecimalUnit subclass if not issubclass(currency_unit_class, DecimalUnit): msg = ( f"CURRENCY_UNIT must be a subclass of DecimalUnit, " f"but {settings.CURRENCY_UNIT_PATH} is not." ) sys.exit(msg) # Set it globally - Currency/RealWorldCurrency __new__ will use this settings.CURRENCY_UNIT_CLASS = currency_unit_class except (ValueError, ImportError, AttributeError) as e: msg = f"Failed to import CURRENCY_UNIT '{settings.CURRENCY_UNIT_PATH}': {e}" sys.exit(msg) 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 # note-to-self # 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 send_termination_notice(PORT) -> int: # note-to-self # expensive imports. do it in here so that we don't delay devserver_inner from urllib.request import urlopen import urllib.error try: # note-to-self # 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: # note-to-self # - 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, # note-to-self # 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 resetdb startapp startapp_trials startproject test unzip zip zipserver '''