beatsaber-playlist-tool/docs/prompts/03-scratchpad.md

20 KiB

New Python Class

We are working on this new Python class:

import json
import os
import logging
from datetime import datetime, timedelta
from typing import Optional, Dict, Any

from clients.beatleader import client as beatleader_client
from clients.beatleader.api.player_scores import player_scores_get_compact_scores
from clients.beatleader.models.compact_score_response_response_with_metadata import CompactScoreResponseResponseWithMetadata 

logging.basicConfig(
    format='%(asctime)s %(levelname)s: %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    level=logging.DEBUG
)

class BeatLeaderAPI:
    BASE_URL = "https://api.beatleader.xyz"

    def __init__(self, cache_expiry_days: int = 1, cache_dir: Optional[str] = None):
        self.client = beatleader_client.Client(base_url=self.BASE_URL)
        self.cache_expiry_days = cache_expiry_days
        self.CACHE_DIR = cache_dir or self._determine_cache_dir()
        if not os.path.exists(self.CACHE_DIR):
            os.makedirs(self.CACHE_DIR)
            logging.info(f"Created cache directory: {self.CACHE_DIR}")

    def _determine_cache_dir(self) -> str:
        home_cache = os.path.expanduser("~/.cache")
        beatleader_cache = os.path.join(home_cache, "beatleader")
        
        if os.path.exists(home_cache):
            if not os.path.exists(beatleader_cache):
                try:
                    os.makedirs(beatleader_cache)
                    logging.info(f"Created cache directory: {beatleader_cache}")
                except OSError as e:
                    logging.warning(f"Failed to create {beatleader_cache}: {e}")
                    return os.path.join(os.getcwd(), ".cache")
            return beatleader_cache
        else:
            logging.info("~/.cache doesn't exist, using local .cache directory")
            return os.path.join(os.getcwd(), ".cache")

    def _get_cache_filename(self, player_id: str) -> str:
        return os.path.join(self.CACHE_DIR, f"player_{player_id}_scores.json")

    def _is_cache_valid(self, cache_file: str) -> bool:
        if not os.path.exists(cache_file):
            return False
        file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
        return datetime.now() - file_modified_time < timedelta(days=self.cache_expiry_days)

    def get_player_scores(
        self, 
        player_id: str, 
        use_cache: bool = True, 
        count: int = 100, 
        sort: str = "recent", 
        max_pages: Optional[int] = None
    ) -> Dict[str, Any]:
        """
        Fetches all player scores for a given player ID, handling pagination and caching.

        :param player_id: The ScoreSaber player ID.
        :param use_cache: Whether to use cached data if available.
        :param limit: Number of scores per page.
        :param sort: Sorting criteria.
        :param max_pages: Maximum number of pages to fetch. Fetch all if None.
        :return: A dictionary containing metadata and a list of player scores.
        """
        cache_file = self._get_cache_filename(player_id)

        if use_cache and self._is_cache_valid(cache_file):
            logging.debug(f"Using cached data for player {player_id}")
            with open(cache_file, 'r') as f:
                return json.load(f)

        logging.debug(f"Fetching fresh data for player {player_id}")
        
        all_scores = []
        page = 1
        total_items = None

        while max_pages is None or page <= max_pages:
            try:
                response: CompactScoreResponseResponseWithMetadata = player_scores_get_compact_scores.sync(
                    client=self.client,
                    id=player_id,
                    page=page,
                    count=count,
                    sort=sort
                )

            except Exception as e:
                logging.error(f"Error fetching page {page} for player {player_id}: {e}")
                return {"metadata": {}, "playerScores": []}

            all_scores.extend(response.data)

            if total_items is None:
                total_items = response.metadata.total
                logging.debug(f"Total scores to fetch: {total_items}")

            logging.debug(f"Fetched page {page}: {len(response.data)} scores")

            if len(all_scores) >= total_items:
                break

            page += 1

        result = {
            'metadata': {
                'itemsPerPage': response.metadata.items_per_page,
                'page': response.metadata.page,
                'total': response.metadata.total
            },
            'playerScores': all_scores
        }

        with open(cache_file, 'w') as f:
            json.dump(result, f, default=str)  # default=str to handle datetime serialization

        logging.info(f"Cached scores for player {player_id} at {cache_file}")

        return result

Here is src/clients/beatleader/api/player_scores/player_scores_get_compact_scores.py:

from http import HTTPStatus
from typing import Any, Dict, Optional, Union, cast

import httpx

from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.compact_score_response_response_with_metadata import CompactScoreResponseResponseWithMetadata
from ...models.difficulty_status import DifficultyStatus
from ...models.leaderboard_contexts import LeaderboardContexts
from ...models.order import Order
from ...models.requirements import Requirements
from ...models.score_filter_status import ScoreFilterStatus
from ...models.scores_sort_by import ScoresSortBy
from ...types import UNSET, Response, Unset


def _get_kwargs(
    id: str,
    *,
    sort_by: Union[Unset, ScoresSortBy] = UNSET,
    order: Union[Unset, Order] = UNSET,
    page: Union[Unset, int] = 1,
    count: Union[Unset, int] = 8,
    search: Union[Unset, str] = UNSET,
    diff: Union[Unset, str] = UNSET,
    mode: Union[Unset, str] = UNSET,
    requirements: Union[Unset, Requirements] = UNSET,
    score_status: Union[Unset, ScoreFilterStatus] = UNSET,
    leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
    type: Union[Unset, DifficultyStatus] = UNSET,
    modifiers: Union[Unset, str] = UNSET,
    stars_from: Union[Unset, float] = UNSET,
    stars_to: Union[Unset, float] = UNSET,
    time_from: Union[Unset, int] = UNSET,
    time_to: Union[Unset, int] = UNSET,
    event_id: Union[Unset, int] = UNSET,
) -> Dict[str, Any]:
    params: Dict[str, Any] = {}

    json_sort_by: Union[Unset, str] = UNSET
    if not isinstance(sort_by, Unset):
        json_sort_by = sort_by.value

    params["sortBy"] = json_sort_by

    json_order: Union[Unset, str] = UNSET
    if not isinstance(order, Unset):
        json_order = order.value

    params["order"] = json_order

    params["page"] = page

    params["count"] = count

    params["search"] = search

    params["diff"] = diff

    params["mode"] = mode

    json_requirements: Union[Unset, str] = UNSET
    if not isinstance(requirements, Unset):
        json_requirements = requirements.value

    params["requirements"] = json_requirements

    json_score_status: Union[Unset, str] = UNSET
    if not isinstance(score_status, Unset):
        json_score_status = score_status.value

    params["scoreStatus"] = json_score_status

    json_leaderboard_context: Union[Unset, str] = UNSET
    if not isinstance(leaderboard_context, Unset):
        json_leaderboard_context = leaderboard_context.value

    params["leaderboardContext"] = json_leaderboard_context

    json_type: Union[Unset, str] = UNSET
    if not isinstance(type, Unset):
        json_type = type.value

    params["type"] = json_type

    params["modifiers"] = modifiers

    params["stars_from"] = stars_from

    params["stars_to"] = stars_to

    params["time_from"] = time_from

    params["time_to"] = time_to

    params["eventId"] = event_id

    params = {k: v for k, v in params.items() if v is not UNSET and v is not None}

    _kwargs: Dict[str, Any] = {
        "method": "get",
        "url": f"/player/{id}/scores/compact",
        "params": params,
    }

    return _kwargs


def _parse_response(
    *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    if response.status_code == HTTPStatus.OK:
        response_200 = CompactScoreResponseResponseWithMetadata.from_dict(response.json())

        return response_200
    if response.status_code == HTTPStatus.BAD_REQUEST:
        response_400 = cast(Any, None)
        return response_400
    if response.status_code == HTTPStatus.NOT_FOUND:
        response_404 = cast(Any, None)
        return response_404
    if client.raise_on_unexpected_status:
        raise errors.UnexpectedStatus(response.status_code, response.content)
    else:
        return None


def _build_response(
    *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    return Response(
        status_code=HTTPStatus(response.status_code),
        content=response.content,
        headers=response.headers,
        parsed=_parse_response(client=client, response=response),
    )


def sync_detailed(
    id: str,
    *,
    client: Union[AuthenticatedClient, Client],
    sort_by: Union[Unset, ScoresSortBy] = UNSET,
    order: Union[Unset, Order] = UNSET,
    page: Union[Unset, int] = 1,
    count: Union[Unset, int] = 8,
    search: Union[Unset, str] = UNSET,
    diff: Union[Unset, str] = UNSET,
    mode: Union[Unset, str] = UNSET,
    requirements: Union[Unset, Requirements] = UNSET,
    score_status: Union[Unset, ScoreFilterStatus] = UNSET,
    leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
    type: Union[Unset, DifficultyStatus] = UNSET,
    modifiers: Union[Unset, str] = UNSET,
    stars_from: Union[Unset, float] = UNSET,
    stars_to: Union[Unset, float] = UNSET,
    time_from: Union[Unset, int] = UNSET,
    time_to: Union[Unset, int] = UNSET,
    event_id: Union[Unset, int] = UNSET,
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    """Retrieve player's scores in a compact form

     Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
    processing time

    Args:
        id (str):
        sort_by (Union[Unset, ScoresSortBy]):
        order (Union[Unset, Order]): Represents the order in which values will be sorted.
        page (Union[Unset, int]):  Default: 1.
        count (Union[Unset, int]):  Default: 8.
        search (Union[Unset, str]):
        diff (Union[Unset, str]):
        mode (Union[Unset, str]):
        requirements (Union[Unset, Requirements]):
        score_status (Union[Unset, ScoreFilterStatus]):
        leaderboard_context (Union[Unset, LeaderboardContexts]):
        type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
        modifiers (Union[Unset, str]):
        stars_from (Union[Unset, float]):
        stars_to (Union[Unset, float]):
        time_from (Union[Unset, int]):
        time_to (Union[Unset, int]):
        event_id (Union[Unset, int]):

    Raises:
        errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
        httpx.TimeoutException: If the request takes longer than Client.timeout.

    Returns:
        Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
    """

    kwargs = _get_kwargs(
        id=id,
        sort_by=sort_by,
        order=order,
        page=page,
        count=count,
        search=search,
        diff=diff,
        mode=mode,
        requirements=requirements,
        score_status=score_status,
        leaderboard_context=leaderboard_context,
        type=type,
        modifiers=modifiers,
        stars_from=stars_from,
        stars_to=stars_to,
        time_from=time_from,
        time_to=time_to,
        event_id=event_id,
    )

    response = client.get_httpx_client().request(
        **kwargs,
    )

    return _build_response(client=client, response=response)


def sync(
    id: str,
    *,
    client: Union[AuthenticatedClient, Client],
    sort_by: Union[Unset, ScoresSortBy] = UNSET,
    order: Union[Unset, Order] = UNSET,
    page: Union[Unset, int] = 1,
    count: Union[Unset, int] = 8,
    search: Union[Unset, str] = UNSET,
    diff: Union[Unset, str] = UNSET,
    mode: Union[Unset, str] = UNSET,
    requirements: Union[Unset, Requirements] = UNSET,
    score_status: Union[Unset, ScoreFilterStatus] = UNSET,
    leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
    type: Union[Unset, DifficultyStatus] = UNSET,
    modifiers: Union[Unset, str] = UNSET,
    stars_from: Union[Unset, float] = UNSET,
    stars_to: Union[Unset, float] = UNSET,
    time_from: Union[Unset, int] = UNSET,
    time_to: Union[Unset, int] = UNSET,
    event_id: Union[Unset, int] = UNSET,
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    """Retrieve player's scores in a compact form

     Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
    processing time

    Args:
        id (str):
        sort_by (Union[Unset, ScoresSortBy]):
        order (Union[Unset, Order]): Represents the order in which values will be sorted.
        page (Union[Unset, int]):  Default: 1.
        count (Union[Unset, int]):  Default: 8.
        search (Union[Unset, str]):
        diff (Union[Unset, str]):
        mode (Union[Unset, str]):
        requirements (Union[Unset, Requirements]):
        score_status (Union[Unset, ScoreFilterStatus]):
        leaderboard_context (Union[Unset, LeaderboardContexts]):
        type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
        modifiers (Union[Unset, str]):
        stars_from (Union[Unset, float]):
        stars_to (Union[Unset, float]):
        time_from (Union[Unset, int]):
        time_to (Union[Unset, int]):
        event_id (Union[Unset, int]):

    Raises:
        errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
        httpx.TimeoutException: If the request takes longer than Client.timeout.

    Returns:
        Union[Any, CompactScoreResponseResponseWithMetadata]
    """

    return sync_detailed(
        id=id,
        client=client,
        sort_by=sort_by,
        order=order,
        page=page,
        count=count,
        search=search,
        diff=diff,
        mode=mode,
        requirements=requirements,
        score_status=score_status,
        leaderboard_context=leaderboard_context,
        type=type,
        modifiers=modifiers,
        stars_from=stars_from,
        stars_to=stars_to,
        time_from=time_from,
        time_to=time_to,
        event_id=event_id,
    ).parsed


async def asyncio_detailed(
    id: str,
    *,
    client: Union[AuthenticatedClient, Client],
    sort_by: Union[Unset, ScoresSortBy] = UNSET,
    order: Union[Unset, Order] = UNSET,
    page: Union[Unset, int] = 1,
    count: Union[Unset, int] = 8,
    search: Union[Unset, str] = UNSET,
    diff: Union[Unset, str] = UNSET,
    mode: Union[Unset, str] = UNSET,
    requirements: Union[Unset, Requirements] = UNSET,
    score_status: Union[Unset, ScoreFilterStatus] = UNSET,
    leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
    type: Union[Unset, DifficultyStatus] = UNSET,
    modifiers: Union[Unset, str] = UNSET,
    stars_from: Union[Unset, float] = UNSET,
    stars_to: Union[Unset, float] = UNSET,
    time_from: Union[Unset, int] = UNSET,
    time_to: Union[Unset, int] = UNSET,
    event_id: Union[Unset, int] = UNSET,
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    """Retrieve player's scores in a compact form

     Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
    processing time

    Args:
        id (str):
        sort_by (Union[Unset, ScoresSortBy]):
        order (Union[Unset, Order]): Represents the order in which values will be sorted.
        page (Union[Unset, int]):  Default: 1.
        count (Union[Unset, int]):  Default: 8.
        search (Union[Unset, str]):
        diff (Union[Unset, str]):
        mode (Union[Unset, str]):
        requirements (Union[Unset, Requirements]):
        score_status (Union[Unset, ScoreFilterStatus]):
        leaderboard_context (Union[Unset, LeaderboardContexts]):
        type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
        modifiers (Union[Unset, str]):
        stars_from (Union[Unset, float]):
        stars_to (Union[Unset, float]):
        time_from (Union[Unset, int]):
        time_to (Union[Unset, int]):
        event_id (Union[Unset, int]):

    Raises:
        errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
        httpx.TimeoutException: If the request takes longer than Client.timeout.

    Returns:
        Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
    """

    kwargs = _get_kwargs(
        id=id,
        sort_by=sort_by,
        order=order,
        page=page,
        count=count,
        search=search,
        diff=diff,
        mode=mode,
        requirements=requirements,
        score_status=score_status,
        leaderboard_context=leaderboard_context,
        type=type,
        modifiers=modifiers,
        stars_from=stars_from,
        stars_to=stars_to,
        time_from=time_from,
        time_to=time_to,
        event_id=event_id,
    )

    response = await client.get_async_httpx_client().request(**kwargs)

    return _build_response(client=client, response=response)


async def asyncio(
    id: str,
    *,
    client: Union[AuthenticatedClient, Client],
    sort_by: Union[Unset, ScoresSortBy] = UNSET,
    order: Union[Unset, Order] = UNSET,
    page: Union[Unset, int] = 1,
    count: Union[Unset, int] = 8,
    search: Union[Unset, str] = UNSET,
    diff: Union[Unset, str] = UNSET,
    mode: Union[Unset, str] = UNSET,
    requirements: Union[Unset, Requirements] = UNSET,
    score_status: Union[Unset, ScoreFilterStatus] = UNSET,
    leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
    type: Union[Unset, DifficultyStatus] = UNSET,
    modifiers: Union[Unset, str] = UNSET,
    stars_from: Union[Unset, float] = UNSET,
    stars_to: Union[Unset, float] = UNSET,
    time_from: Union[Unset, int] = UNSET,
    time_to: Union[Unset, int] = UNSET,
    event_id: Union[Unset, int] = UNSET,
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
    """Retrieve player's scores in a compact form

     Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
    processing time

    Args:
        id (str):
        sort_by (Union[Unset, ScoresSortBy]):
        order (Union[Unset, Order]): Represents the order in which values will be sorted.
        page (Union[Unset, int]):  Default: 1.
        count (Union[Unset, int]):  Default: 8.
        search (Union[Unset, str]):
        diff (Union[Unset, str]):
        mode (Union[Unset, str]):
        requirements (Union[Unset, Requirements]):
        score_status (Union[Unset, ScoreFilterStatus]):
        leaderboard_context (Union[Unset, LeaderboardContexts]):
        type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
        modifiers (Union[Unset, str]):
        stars_from (Union[Unset, float]):
        stars_to (Union[Unset, float]):
        time_from (Union[Unset, int]):
        time_to (Union[Unset, int]):
        event_id (Union[Unset, int]):

    Raises:
        errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
        httpx.TimeoutException: If the request takes longer than Client.timeout.

    Returns:
        Union[Any, CompactScoreResponseResponseWithMetadata]
    """

    return (
        await asyncio_detailed(
            id=id,
            client=client,
            sort_by=sort_by,
            order=order,
            page=page,
            count=count,
            search=search,
            diff=diff,
            mode=mode,
            requirements=requirements,
            score_status=score_status,
            leaderboard_context=leaderboard_context,
            type=type,
            modifiers=modifiers,
            stars_from=stars_from,
            stars_to=stars_to,
            time_from=time_from,
            time_to=time_to,
            event_id=event_id,
        )
    ).parsed

Please review get_player_scores(), we wonder if the sort option is done correctly.