def paginate(games_it: ResultIteratorExt[ApexGameSummary], username: Optional[str] = None, page_size: int = PAGINATION_SIZE): games = list(islice(games_it, page_size)) if games_it.last_evaluated_key: next_args = MultiDict(request.args) next_args['last_evaluated'] = b64_encode( json.dumps(games_it.last_evaluated_key)) if username: next_args['username'] = username next_from = url_for('apex.games_list.games_pagination', **next_args) else: next_from = None return games, next_from
def get_sessions( user: User, share_settings: Optional[OverwatchShareSettings] = None, page_minimum_size: int = PAGINATION_PAGE_MINIMUM_SIZE, sessions_count_as: int = PAGINATION_SESSIONS_COUNT_AS, limit: Optional[int] = None, ) -> Tuple[List[Session], Optional[Season], bool, Optional[str]]: logger.info(f'Fetching games for user={user.user_id}: {user.username!r}') # merge actual args with parent page args from intercooler (for when this is a pagination fetch) logger.info(f'Request args={request.args}') args = parse_args(request.args.get('ic-current-url')) logger.info(f'Intercooler args={args}') args.update(request.args) logger.info(f'Merged args={args}') logger.info(f'Share settings={share_settings}') # Use season as {specified season, user's last season, current season} in that order # Note: when getting sessions for a share link, where the user has played in a new season, but has no visible games will generage an # empty page with no way of the viewer knowing which seasons have games. This could be detected, and we could compute the valid seasons # for a share link, but... if hopeful_int(args.get('season')) in user.overwatch_seasons: season = overwatch_data.seasons[int(args['season'])] logger.info(f'Using season={season.index} from args') elif user.overwatch_last_season in overwatch_data.seasons: season = overwatch_data.seasons[user.overwatch_last_season] logger.info( f'Using season={season.index} from user.overwatch_last_season') else: season = overwatch_data.current_season logger.info(f'Using season={season.index} from current_season') # Use include_quickplay from {share settings, specified season, cookie} in that order, defaulting to True if not set in any if share_settings and not share_settings.include_quickplay: logger.info(f'Using include_quickplay=False from share settings') include_quickplay = False elif hopeful_int(args.get('quickplay')) in [0, 1]: include_quickplay = bool(int(args['quickplay'])) logger.info( f'Using include_quickplay={include_quickplay} from request') else: include_quickplay = int(request.cookies.get('include_quickplay', 1)) logger.info(f'Using include_quickplay={include_quickplay} from cookie') # Use a range key filter for the season logger.info( f'Using season time range {datetime.datetime.fromtimestamp(season.start)} -> {datetime.datetime.fromtimestamp(season.end)}' ) range_key_condition = OverwatchGameSummary.time.between( season.start, season.end) # Construct the filter condition combining season, share accounts, show quickplay if season.index is not None: filter_condition = OverwatchGameSummary.season == season.index else: filter_condition = OverwatchGameSummary.key.exists() if share_settings and share_settings.accounts: logger.info( f'Share settings has whitelisted accounts {share_settings.accounts}' ) filter_condition &= OverwatchGameSummary.player_name.is_in( *share_settings.accounts) if not share_settings and hopeful_int(args.get('custom_games')) == 1: logger.info( f'Returning custom games from share_settings={share_settings}, custom_games={args.get("custom_games")}' ) filter_condition &= (OverwatchGameSummary.game_type == 'custom') elif not include_quickplay: filter_condition &= ( OverwatchGameSummary.game_type == 'competitive' ) | OverwatchGameSummary.game_type.does_not_exist() else: filter_condition &= OverwatchGameSummary.game_type.is_in( 'quickplay', 'competitive') | OverwatchGameSummary.game_type.does_not_exist() # Use last_evaluated from args if 'last_evaluated' in args: last_evaluated = json.loads(b64_decode(request.args['last_evaluated'])) logger.info(f'Using last_evaluated={last_evaluated}') else: last_evaluated = None logger.info(f'Using last_evaluated={last_evaluated}') # Use a page size that is slightly larger than the minimum number of elements we want, to avoid having to use 2 pages page_size = page_minimum_size + 15 logger.info( f'Getting games for user_id={user.user_id}, range_key_condition={range_key_condition}, filter_condition={filter_condition}, ' f'last_evaluated={last_evaluated}, page_size={page_size}') t0 = time.perf_counter() sessions: List[Session] = [] total_games = 0 last_evaluated_key = None query = OverwatchGameSummary.user_id_time_index.query( user.user_id, range_key_condition, filter_condition, newest_first=True, last_evaluated_key=last_evaluated, page_size=page_size, limit=limit, ) for game in query: if sessions and sessions[-1].add_game(game): total_games += 1 logger.debug( f' ' f'Added game to last session, ' f'offset={s2ts(sessions[-1].games[-2].time - (game.time + game.duration))}, ' f'game={game}') elif total_games + len( sessions) * sessions_count_as <= page_minimum_size: sessions.append(Session(game)) total_games += 1 logger.debug(f'Added new session {sessions[-1]}, game={game}') else: logger.info( f'Got {total_games} games over {len(sessions)} sessions - pagination limit reached' ) break last_evaluated_key = query.last_evaluated_key else: last_evaluated_key = None t1 = time.perf_counter() logger.info( f'Building sessions list took {(t1 - t0)*1000:.2f}ms - ' f'took {total_games / page_size + 0.5:.0f} result pages ({(total_games / page_size)*100:.0f}% of page used)' ) logger.info(f'Got {len(sessions)} sessions:') for s in sessions: logger.info(f' {s}') if last_evaluated_key is None: logger.info(f'Reached end of query - not providing a last_evaluated') return sessions, season, include_quickplay, None else: logger.info( f'Reached end of query with items remaining - returning last_evaluated={last_evaluated_key!r}' ) return sessions, season, include_quickplay, b64_encode( json.dumps(last_evaluated_key))
def get_sessions( user: User, share_link: Optional[ShareLink] = None, page_minimum_size: int = PAGINATION_PAGE_MINIMUM_SIZE, sessions_count_as: int = PAGINATION_SESSIONS_COUNT_AS, ) -> Tuple[List[Session], Optional[Season], Optional[str]]: if hopeful_int(request.args.get('season')) in user.overwatch_seasons: # called from /games - parse season from ?season=N logger.info(f'Using season from request args: {request.args}') season = overwatch_data.seasons[int(request.args['season'])] elif hopeful_int( parse_args(request.args.get('ic-current-url')).get( 'season')) in user.overwatch_seasons: # called from intercooler pagination - parse season from ?ic-current-url='/overtwatch/games?season=N' logger.info( f'Using season from ic-current-url args: {parse_args(request.args["ic-current-url"])}' ) season = overwatch_data.seasons[int( parse_args(request.args['ic-current-url'])['season'])] elif user.overwatch_last_season: logger.info( f'Using season from user.overwatch_last_season: {user.overwatch_last_season}' ) season = overwatch_data.seasons[user.overwatch_last_season] else: logger.info(f'Using season from current_season') season = overwatch_data.current_season logger.info(f'Getting games for {user.username} => season={season}') season = overwatch_data.seasons[season.index] range_key_condition = OverwatchGameSummary.time.between( season.start, season.end) filter_condition = OverwatchGameSummary.season == season.index if share_link: logger.info( f'Share link {share_link.share_key!r} has whitelisted accounts {share_link.player_name_filter}' ) filter_condition &= OverwatchGameSummary.player_name.is_in( *share_link.player_name_filter) hide_quickplay = int(request.cookies.get('hide_quickplay', 0)) logger.info(f'hide_quickplay={hide_quickplay}') if hide_quickplay: filter_condition &= OverwatchGameSummary.game_type == 'competitive' else: filter_condition &= OverwatchGameSummary.game_type.is_in( 'quickplay', 'competitive') if 'last_evaluated' in request.args: last_evaluated = json.loads(b64_decode(request.args['last_evaluated'])) else: last_evaluated = None # Set the limit past the minimum games by the 95percentile of session lengths. # This means we can (usually) load the full session that puts the total game count over # minimum_required_games without incurring another fetch page_size = page_minimum_size + 20 logger.info( f'Getting games for {user.username}: {user.user_id}, {range_key_condition}, {filter_condition} ' f'with last_evaluated={last_evaluated} and page_size={page_size}') t0 = time.perf_counter() sessions: List[Session] = [] total_games = 0 last_evaluated_key = None query = OverwatchGameSummary.user_id_time_index.query( user.user_id, range_key_condition, filter_condition, newest_first=True, last_evaluated_key=last_evaluated, page_size=page_size, ) for game in query: if sessions and sessions[-1].add_game(game): total_games += 1 logger.info( f' ' f'Added game to last session, ' f'offset={s2ts(sessions[-1].games[-2].time - (game.time + game.duration))}, ' f'game={game}') elif total_games + len( sessions) * sessions_count_as <= page_minimum_size: sessions.append(Session(game)) total_games += 1 logger.info(f'Added new session {sessions[-1]}, game={game}') else: logger.info( f'Got {total_games} games over {len(sessions)} sessions - pagination limit reached' ) break last_evaluated_key = query.last_evaluated_key else: last_evaluated_key = None t1 = time.perf_counter() logger.info( f'Building sessions list took {(t1 - t0)*1000:.2f}ms - took {total_games / page_size + 0.5:.0f} result pages' ) logger.info(f'Got {len(sessions)} sessions:') for s in sessions: logger.info(f' {s}') if last_evaluated_key is None: logger.info(f'Reached end of query - not providing a last_evaluated') return sessions, season, None else: logger.info( f'Reached end of query with items remaining - returning last_evaluated={last_evaluated_key!r}' ) return sessions, season, b64_encode(json.dumps(last_evaluated_key))
def get_sessions( user: User, page_minimum_size: int = PAGINATION_PAGE_MINIMUM_SIZE, sessions_count_as: int = PAGINATION_SESSIONS_COUNT_AS, ) -> Tuple[List[Session], Optional[str]]: logger.info(f'Fetching games for user={user.user_id}: {user.username!r}') # merge actual args with parent page args from intercooler (for when this is a pagination fetch) logger.info(f'Request args={request.args}') args = parse_args(request.args.get('ic-current-url')) logger.info(f'Intercooler args={args}') args.update(request.args) logger.info(f'Merged args={args}') # Construct the filter condition combining season, share accounts, show quickplay # filter_condition = ValorantGameSummary.season_mode_id == 0 filter_condition = None range_key_condition = None # Use last_evaluated from args if 'last_evaluated' in args: last_evaluated = json.loads(b64_decode(request.args['last_evaluated'])) logger.info(f'Using last_evaluated={last_evaluated}') else: last_evaluated = None logger.info(f'Using last_evaluated={last_evaluated}') # Use a page size that is slightly larger than the minimum number of elements we want, to avoid having to use 2 pages page_size = page_minimum_size + 15 logger.info( f'Getting games for user_id={user.user_id}, range_key_condition={range_key_condition}, filter_condition={filter_condition}, ' f'last_evaluated={last_evaluated}, page_size={page_size}') t0 = time.perf_counter() sessions: List[Session] = [] total_games = 0 last_evaluated_key = None query = ValorantGameSummary.user_id_timestamp_index.query( user.user_id, range_key_condition, filter_condition, newest_first=True, last_evaluated_key=last_evaluated, page_size=page_size, ) for game in query: if sessions and sessions[-1].add_game(game): total_games += 1 logger.debug( f' ' f'Added game to last session, ' f'offset={s2ts(sessions[-1].games[-2].timestamp - (game.timestamp + game.duration))}, ' f'game={game}') elif total_games + len( sessions) * sessions_count_as <= page_minimum_size: sessions.append(Session(game)) total_games += 1 logger.debug(f'Added new session {sessions[-1]}, game={game}') else: logger.info( f'Got {total_games} games over {len(sessions)} sessions - pagination limit reached' ) break last_evaluated_key = query.last_evaluated_key else: last_evaluated_key = None t1 = time.perf_counter() logger.info( f'Building sessions list took {(t1 - t0)*1000:.2f}ms - ' f'took {total_games / page_size + 0.5:.0f} result pages ({(total_games / page_size)*100:.0f}% of page used)' ) logger.info(f'Got {len(sessions)} sessions:') for s in sessions: logger.info(f' {s}') if last_evaluated_key is None: logger.info(f'Reached end of query - not providing a last_evaluated') return sessions, None else: logger.info( f'Reached end of query with items remaining - returning last_evaluated={last_evaluated_key!r}' ) return sessions, b64_encode(json.dumps(last_evaluated_key))