import datetime import logging import os from collections import OrderedDict, defaultdict from pathlib import Path from typing import List from django.conf import settings import otree.common import otree.export import otree.session from otree.constants import AUTO_NAME_BOTS_EXPORT_FOLDER from otree.models import Session, Participant from otree.session import SESSION_CONFIGS_DICT from .bot import ParticipantBot from otree.common import get_bots_module, get_models_module logger = logging.getLogger(__name__) class SessionBotRunner: def __init__(self, bots: List[ParticipantBot]): self.bots = {} for bot in bots: self.bots[bot.participant_code] = bot def play(self): '''round-robin''' self.open_start_urls() loops_without_progress = 0 while True: if len(self.bots) == 0: return # bots got stuck if there's 2 wait pages in a row if loops_without_progress > 10: raise AssertionError('Bots got stuck') # store in a separate list so we don't mutate the iterable playable_ids = list(self.bots.keys()) progress_made = False for pk in playable_ids: bot = self.bots[pk] if bot.on_wait_page(): pass else: try: submission = next(bot.submits_generator) except StopIteration: # this bot is finished self.bots.pop(pk) else: bot.submit(**submission) progress_made = True # need to set this so that we only count *consecutive* unsuccessful loops # see Manu's error on 2019-12-19. loops_without_progress = 0 if not progress_made: loops_without_progress += 1 def open_start_urls(self): for bot in self.bots.values(): bot.open_start_url() def make_bots(*, session_pk, case_number, use_browser_bots) -> List[ParticipantBot]: update_kwargs = {'_is_bot': True} if use_browser_bots: update_kwargs['is_browser_bot'] = True Participant.objects.filter(session_id=session_pk).update(**update_kwargs) bots = [] # can't use .distinct('player_pk') because it only works on Postgres # this implicitly orders by round also session = Session.objects.get(pk=session_pk) participant_codes = session.participant_set.order_by('id').values_list( 'code', flat=True ) player_bots_dict = {pcode: [] for pcode in participant_codes} for app_name in session.config['app_sequence']: bots_module = get_bots_module(app_name) models_module = get_models_module(app_name) players = ( models_module.Player.objects.filter(session_id=session_pk) .order_by('round_number') .values('id', 'participant_id', 'participant__code', 'subsession_id') ) for player in players: participant_code = player['participant__code'] player_bot = bots_module.PlayerBot( case_number=case_number, app_name=app_name, player_pk=player['id'], subsession_pk=player['subsession_id'], participant_code=participant_code, session_pk=session_pk, ) player_bots_dict[participant_code].append(player_bot) executed_live_methods = set() for participant_code, player_bots in player_bots_dict.items(): bot = ParticipantBot( participant_code, player_bots=player_bots, executed_live_methods=executed_live_methods, ) bots.append(bot) return bots def run_bots(session: Session, case_number=None): bot_list = make_bots( session_pk=session.pk, case_number=case_number, use_browser_bots=False ) if session.get_room() is None: session.mock_exogenous_data() session.save() runner = SessionBotRunner(bots=bot_list) runner.play() def run_all_bots_for_session_config(session_config_name, num_participants, export_path): """ this means all test cases are in 1 big test case. so if 1 fails, the others will not get run. """ if session_config_name: session_config_names = [session_config_name] else: session_config_names = SESSION_CONFIGS_DICT.keys() for config_name in session_config_names: try: config = SESSION_CONFIGS_DICT[config_name] except KeyError: # important to alert the user, since people might be trying to enter app names. msg = f"No session config with name '{config_name}'." raise Exception(msg) from None num_bot_cases = config.get_num_bot_cases() for case_number in range(num_bot_cases): logger.info( "Creating '{}' session (test case {})".format(config_name, case_number) ) session = otree.session.create_session( session_config_name=config_name, num_participants=(num_participants or config['num_demo_participants']), ) run_bots(session, case_number=case_number) logger.info('Bots completed session') if export_path: now = datetime.datetime.now() if export_path == AUTO_NAME_BOTS_EXPORT_FOLDER: # oTree convention to prefix __temp all temp folders. export_path = now.strftime('__temp_bots_%b%d_%Hh%Mm%S.%f')[:-5] + 's' os.makedirs(export_path, exist_ok=True) for app in settings.INSTALLED_OTREE_APPS: model_module = otree.common.get_models_module(app) if model_module.Player.objects.exists(): fpath = Path(export_path, "{}.csv".format(app)) with fpath.open("w", encoding="utf8") as fp: otree.export.export_app(app, fp) fpath = Path(export_path, "all_apps_wide.csv") with fpath.open("w", encoding="utf8") as fp: otree.export.export_wide(fp) logger.info('Exported CSV to folder "{}"'.format(export_path)) else: logger.info( 'Tip: Run this command with the --export flag' ' to save the data generated by bots.' )