# 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. ```python 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= 2024-10-04 10:18:23 DEBUG: start_tls.started ssl_context= server_hostname='api.beatleader.xyz' timeout=None 2024-10-04 10:18:23 DEBUG: start_tls.complete return_value= 2024-10-04 10:18:23 DEBUG: send_request_headers.started request= 2024-10-04 10:18:23 DEBUG: send_request_headers.complete 2024-10-04 10:18:23 DEBUG: send_request_body.started request= 2024-10-04 10:18:23 DEBUG: send_request_body.complete 2024-10-04 10:18:23 DEBUG: receive_response_headers.started request= 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= 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=, 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.