import datetime import logging import os from pathlib import Path from typing import List import otree.common import otree.export import otree.session from otree import settings from otree.common import get_bots_module, get_models_module from otree.constants import AUTO_NAME_BOTS_EXPORT_FOLDER from otree.database import values_flat, db from otree.models import Session, Participant from otree.session import SESSION_CONFIGS_DICT from .bot import ParticipantBot 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 id in playable_ids: bot = self.bots[id] if bot.on_wait_page(): pass else: try: submission = bot.get_next_submit() except StopIteration: # this bot is finished self.bots.pop(id) 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 = {Participant._is_bot: True} if use_browser_bots: update_kwargs[Participant.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(id=session_pk) participant_codes = values_flat(session.pp_set.order_by('id'), Participant.code) 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) Player = models_module.Player players = ( Player.objects_filter(session_id=session_pk) .join(Participant) .order_by('round_number') .with_entities(Player.id, Participant.code, Player.subsession_id) ) for player_id, participant_code, subsession_id in players: player_bot = bots_module.PlayerBot( case_number=case_number, app_name=app_name, player_pk=player_id, subsession_pk=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_id, case_number=None): session = Session.objects_get(id=session_id) bot_list = make_bots( session_pk=session.id, case_number=case_number, use_browser_bots=False ) if session.get_room() is None: session.mock_exogenous_data() db.commit() 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']), ) session_id = session.id run_bots(session_id, 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.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", newline='', 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.' )