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

15 KiB

New Python Class

We are working on a python class that wraps an openapi client generated by openapi-python-client. The class will handle caching and pagination for a specific API endpoint.

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.score_response_with_my_score_response_with_metadata import ScoreResponseWithMyScoreResponseWithMetadata

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)

    """TODO:
    def get_player_scores(
        self, 
        player_id: str, 
        use_cache: bool = True, 
        limit: 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.

        """
        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: ScoreResponseWithMyScoreResponseWithMetadata = player_scores_get_compact_scores.sync(
                    client=self.client,
                    id=player_id,
                    page=page,
                    limit=limit,
                    sort=sort
                )
            except Exception as e:
                logging.error(f"Error fetching page {page} for player {player_id}: {e}")
                return {"metadata": {}, "playerScores": []}

            all_scores.extend([score.dict() for score in response.player_scores])

            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.player_scores)} scores")

            if len(all_scores) >= total_items:
                break

            page += 1

        result = {
            'metadata': response.metadata.dict(),
            '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
    """

This class is a draft and not yet tested. We don't know what the proper attributes for limit and sort are yet. we also just have a mockup of a get_player_scores() method.

Here is a sample of the response data:

`In [1]: 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.score_response_with_my_score_response_with_metadata import ScoreResponseWithMyScoreResponseWithMetadata ...: ...: logging.basicConfig( ...: format='%(asctime)s %(levelname)s: %(message)s', ...: datefmt='%Y-%m-%d %H:%M:%S', ...: level=logging.DEBUG ...: ) ...: ...: player_id = '76561199407393962' ...: ...: BASE_URL = "https://api.beatleader.xyz" ...: client = beatleader_client.Client(base_url=BASE_URL) ...: ...: response: ScoreResponseWithMyScoreResponseWithMetadata = player_scores_get_compact_scores.sync_detailed( ...: client=client, ...: id=player_id) 2024-10-04 10:18:23 DEBUG: load_ssl_context verify=True cert=None trust_env=True http2=False 2024-10-04 10:18:23 DEBUG: load_verify_locations cafile='/home/blee/ops/beatsaber/playlist-tool/.venv/lib/python3.11/site-packages/certifi/cacert.pem' 2024-10-04 10:18:23 DEBUG: connect_tcp.started host='api.beatleader.xyz' port=443 local_address=None timeout=None socket_options=None 2024-10-04 10:18:23 DEBUG: connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7fa79f0d4350> 2024-10-04 10:18:23 DEBUG: start_tls.started ssl_context=<ssl.SSLContext object at 0x7fa79f5b1250> server_hostname='api.beatleader.xyz' timeout=None 2024-10-04 10:18:23 DEBUG: start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7fa79f0f59d0> 2024-10-04 10:18:23 DEBUG: send_request_headers.started request=<Request [b'GET']> 2024-10-04 10:18:23 DEBUG: send_request_headers.complete 2024-10-04 10:18:23 DEBUG: send_request_body.started request=<Request [b'GET']> 2024-10-04 10:18:23 DEBUG: send_request_body.complete 2024-10-04 10:18:23 DEBUG: receive_response_headers.started request=<Request [b'GET']> 2024-10-04 10:18:23 DEBUG: receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Fri, 04 Oct 2024 17:18:23 GMT'), (b'Content-Type', b'application/json; charset=utf-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'X-Rate-Limit-Limit', b'10s'), (b'X-Rate-Limit-Remaining', b'49'), (b'X-Rate-Limit-Reset', b'2024-10-04T17:18:33.2883475Z'), (b'Server-Timing', b'db;dur=88'), (b'CF-Cache-Status', b'DYNAMIC'), (b'Report-To', b'{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2FAFmlHN%2BCrRbTVTzRB3nlDn%2BqlY9h%2B6qtRXYD9M3s%2F8ify3z37wkIRXw0AExoIyf%2BLrAl20fxLf1HGqt7UtMHeW7JZT%2BjqQ9nDmp0zZarZcfOMfVKF%2B9mI8qCuBD1%2BjtBX9CoA%3D%3D"}],"group":"cf-nel","max_age":604800}'), (b'NEL', b'{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'), (b'Server', b'cloudflare'), (b'CF-RAY', b'8cd6d4b329370fbc-LAX'), (b'Content-Encoding', b'gzip')]) 2024-10-04 10:18:23 INFO: HTTP Request: GET https://api.beatleader.xyz/player/76561199407393962/scores/compact?page=1&count=8 "HTTP/1.1 200 OK" 2024-10-04 10:18:23 DEBUG: receive_response_body.started request=<Request [b'GET']> 2024-10-04 10:18:23 DEBUG: receive_response_body.complete 2024-10-04 10:18:23 DEBUG: response_closed.started 2024-10-04 10:18:23 DEBUG: response_closed.complete

2024-10-04 10:18:23 DEBUG: Using selector: EpollSelector In [2]: response Out[2]: Response(status_code=<HTTPStatus.OK: 200>, content=b'{"metadata":{"itemsPerPage":8,"page":1,"total":1110},"data":[{"score":{"id":18223233,"baseScore":188988,"modifiedScore":188988,"modifiers":"","fullCombo":true,"maxCombo":217,"missedNotes":0,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.98229164,"pp":0,"epochTime":1727913725},"leaderboard":{"id":"3ae7f51","songHash":"F4D70651577A6DB4F906762393E0FC6809F22FED","modeName":"Standard","difficulty":5}},{"score":{"id":18222857,"baseScore":937464,"modifiedScore":937464,"modifiers":"","fullCombo":false,"maxCombo":600,"missedNotes":0,"badCuts":1,"hmd":512,"controller":1,"accuracy":0.9416496,"pp":392.4357,"epochTime":1727912395},"leaderboard":{"id":"e29891","songHash":"02e42bb3280e0ea52829a4a2bf47f3eb8a3e32eb","modeName":"Standard","difficulty":9}},{"score":{"id":18222373,"baseScore":503901,"modifiedScore":503901,"modifiers":"PM","fullCombo":true,"maxCombo":578,"missedNotes":0,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.9606989,"pp":216.59628,"epochTime":1727910736},"leaderboard":{"id":"13fe111","songHash":"7bdd78a1787e0fd59a24466d700e1683b1cf5de4","modeName":"Standard","difficulty":1}},{"score":{"id":18222211,"baseScore":307804,"modifiedScore":307804,"modifiers":"","fullCombo":true,"maxCombo":351,"missedNotes":0,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.9750661,"pp":169.61502,"epochTime":1727910142},"leaderboard":{"id":"11c9c11","songHash":"4dfaf66b4a2e78e1b87d7a83634ee322afd270c5","modeName":"Standard","difficulty":1}},{"score":{"id":18222140,"baseScore":107899,"modifiedScore":107899,"modifiers":"","fullCombo":true,"maxCombo":128,"missedNotes":0,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.976329,"pp":157.81444,"epochTime":1727909912},"leaderboard":{"id":"d73511","songHash":"e6e02417e730ad6408fbe6363e99efd462083070","modeName":"Standard","difficulty":1}},{"score":{"id":18203563,"baseScore":937173,"modifiedScore":937173,"modifiers":"","fullCombo":false,"maxCombo":565,"missedNotes":5,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.9234369,"pp":335.5832,"epochTime":1727827105},"leaderboard":{"id":"ca3051","songHash":"d44de2eebd64f3cfa70c024fabb042bf73a43f41","modeName":"Standard","difficulty":5}},{"score":{"id":18202861,"baseScore":1364040,"modifiedScore":1364040,"modifiers":"","fullCombo":false,"maxCombo":678,"missedNotes":6,"badCuts":1,"hmd":512,"controller":1,"accuracy":0.9236989,"pp":337.5511,"epochTime":1727824695},"leaderboard":{"id":"11b4991","songHash":"09f8bee6908e3a9cd724b3db3162a5c381ecb156","modeName":"Standard","difficulty":9}},{"score":{"id":18202637,"baseScore":397686,"modifiedScore":397686,"modifiers":"","fullCombo":true,"maxCombo":456,"missedNotes":0,"badCuts":0,"hmd":512,"controller":1,"accuracy":0.96461344,"pp":0,"epochTime":1727823927},"leaderboard":{"id":"21e2151","songHash":"1262e162a207aa7fbcf18f18eaf5a612a35f4139","modeName":"Standard","difficulty":5}}]}', headers=Headers({'date': 'Fri, 04 Oct 2024 17:18:23 GMT', 'content-type': 'application/json; charset=utf-8', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'vary': 'Accept-Encoding', 'x-rate-limit-limit': '10s', 'x-rate-limit-remaining': '49', 'x-rate-limit-reset': '2024-10-04T17:18:33.2883475Z', 'server-timing': 'db;dur=88', 'cf-cache-status': 'DYNAMIC', 'report-to': '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2FAFmlHN%2BCrRbTVTzRB3nlDn%2BqlY9h%2B6qtRXYD9M3s%2F8ify3z37wkIRXw0AExoIyf%2BLrAl20fxLf1HGqt7UtMHeW7JZT%2BjqQ9nDmp0zZarZcfOMfVKF%2B9mI8qCuBD1%2BjtBX9CoA%3D%3D"}],"group":"cf-nel","max_age":604800}', 'nel': '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}', 'server': 'cloudflare', 'cf-ray': '8cd6d4b329370fbc-LAX', 'content-encoding': 'gzip'}), parsed=CompactScoreResponseResponseWithMetadata(metadata=Metadata(items_per_page=8, page=1, total=1110), data=[{'score': {'id': 18223233, 'baseScore': 188988, 'modifiedScore': 188988, 'modifiers': '', 'fullCombo': True, 'maxCombo': 217, 'missedNotes': 0, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.98229164, 'pp': 0, 'epochTime': 1727913725}, 'leaderboard': {'id': '3ae7f51', 'songHash': 'F4D70651577A6DB4F906762393E0FC6809F22FED', 'modeName': 'Standard', 'difficulty': 5}}, {'score': {'id': 18222857, 'baseScore': 937464, 'modifiedScore': 937464, 'modifiers': '', 'fullCombo': False, 'maxCombo': 600, 'missedNotes': 0, 'badCuts': 1, 'hmd': 512, 'controller': 1, 'accuracy': 0.9416496, 'pp': 392.4357, 'epochTime': 1727912395}, 'leaderboard': {'id': 'e29891', 'songHash': '02e42bb3280e0ea52829a4a2bf47f3eb8a3e32eb', 'modeName': 'Standard', 'difficulty': 9}}, {'score': {'id': 18222373, 'baseScore': 503901, 'modifiedScore': 503901, 'modifiers': 'PM', 'fullCombo': True, 'maxCombo': 578, 'missedNotes': 0, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.9606989, 'pp': 216.59628, 'epochTime': 1727910736}, 'leaderboard': {'id': '13fe111', 'songHash': '7bdd78a1787e0fd59a24466d700e1683b1cf5de4', 'modeName': 'Standard', 'difficulty': 1}}, {'score': {'id': 18222211, 'baseScore': 307804, 'modifiedScore': 307804, 'modifiers': '', 'fullCombo': True, 'maxCombo': 351, 'missedNotes': 0, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.9750661, 'pp': 169.61502, 'epochTime': 1727910142}, 'leaderboard': {'id': '11c9c11', 'songHash': '4dfaf66b4a2e78e1b87d7a83634ee322afd270c5', 'modeName': 'Standard', 'difficulty': 1}}, {'score': {'id': 18222140, 'baseScore': 107899, 'modifiedScore': 107899, 'modifiers': '', 'fullCombo': True, 'maxCombo': 128, 'missedNotes': 0, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.976329, 'pp': 157.81444, 'epochTime': 1727909912}, 'leaderboard': {'id': 'd73511', 'songHash': 'e6e02417e730ad6408fbe6363e99efd462083070', 'modeName': 'Standard', 'difficulty': 1}}, {'score': {'id': 18203563, 'baseScore': 937173, 'modifiedScore': 937173, 'modifiers': '', 'fullCombo': False, 'maxCombo': 565, 'missedNotes': 5, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.9234369, 'pp': 335.5832, 'epochTime': 1727827105}, 'leaderboard': {'id': 'ca3051', 'songHash': 'd44de2eebd64f3cfa70c024fabb042bf73a43f41', 'modeName': 'Standard', 'difficulty': 5}}, {'score': {'id': 18202861, 'baseScore': 1364040, 'modifiedScore': 1364040, 'modifiers': '', 'fullCombo': False, 'maxCombo': 678, 'missedNotes': 6, 'badCuts': 1, 'hmd': 512, 'controller': 1, 'accuracy': 0.9236989, 'pp': 337.5511, 'epochTime': 1727824695}, 'leaderboard': {'id': '11b4991', 'songHash': '09f8bee6908e3a9cd724b3db3162a5c381ecb156', 'modeName': 'Standard', 'difficulty': 9}}, {'score': {'id': 18202637, 'baseScore': 397686, 'modifiedScore': 397686, 'modifiers': '', 'fullCombo': True, 'maxCombo': 456, 'missedNotes': 0, 'badCuts': 0, 'hmd': 512, 'controller': 1, 'accuracy': 0.96461344, 'pp': 0, 'epochTime': 1727823927}, 'leaderboard': {'id': '21e2151', 'songHash': '1262e162a207aa7fbcf18f18eaf5a612a35f4139', 'modeName': 'Standard', 'difficulty': 5}}]))

2024-10-04 10:20:02 DEBUG: Using selector: EpollSelector ``


Please help us finish this wrapper class by implementing the get_player_scores() method.