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))
Beispiel #3
0
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))