544 lines
22 KiB
Markdown
544 lines
22 KiB
Markdown
# Python coding
|
|
|
|
We used openapi-python-client to generate client libraries for the beatleader.xyz api. It's in clients/beatleader in our python project:
|
|
|
|
```
|
|
.
|
|
├── docs/
|
|
│ ├── prompts/
|
|
│ └── *.md
|
|
├── src/
|
|
│ ├── clients/
|
|
│ │ ├── beatleader/
|
|
│ │ │ ├── api/ (various API endpoints)
|
|
│ │ │ ├── models/ (data models)
|
|
│ │ │ └── client.py, errors.py, __init__.py, types.py
|
|
│ │ ├── beatsaver/ (similar structure to beatleader)
|
|
│ │ └── scoresaber/ (similar structure to beatleader)
|
|
│ ├── helpers/
|
|
│ │ └── *.py
|
|
│ └── saberlist/
|
|
│ └── *.py
|
|
├── tests/
|
|
│ ├── assets/
|
|
│ └── playlist_builder.py
|
|
├── pyproject.toml
|
|
└── README.md
|
|
```
|
|
|
|
Here's the clients/beatleader dir:
|
|
|
|
```
|
|
treegit src/clients/beatleader/
|
|
src/clients/beatleader/
|
|
├── api
|
|
│ ├── beast_saber
|
|
│ │ ├── beast_saber_get_all.py
|
|
│ │ ├── beast_saber_nominate.py
|
|
│ │ └── __init__.py
|
|
│ ├── clan
|
|
│ │ ├── clan_get_all.py
|
|
│ │ ├── clan_get_clan_by_id.py
|
|
│ │ ├── clan_get_clan.py
|
|
│ │ ├── clan_get_clan_with_maps_by_id.py
|
|
│ │ ├── clan_get_clan_with_maps.py
|
|
│ │ ├── clan_get_history.py
|
|
│ │ ├── clan_global_map.py
|
|
│ │ └── __init__.py
|
|
│ ├── leaderboard
|
|
│ │ ├── __init__.py
|
|
│ │ ├── leaderboard_get_all.py
|
|
│ │ ├── leaderboard_get_clan_rankings.py
|
|
│ │ ├── leaderboard_get.py
|
|
│ │ └── leaderboard_get_scoregraph.py
|
|
│ ├── modifiers
|
|
│ │ ├── __init__.py
|
|
│ │ └── modifiers_get_modifiers.py
|
|
│ ├── patreon
|
|
│ │ ├── __init__.py
|
|
│ │ └── patreon_refresh_my_patreon.py
|
|
│ ├── player
|
|
│ │ ├── __init__.py
|
|
│ │ ├── player_get_beat_saver.py
|
|
│ │ ├── player_get_discord.py
|
|
│ │ ├── player_get_followers_info.py
|
|
│ │ ├── player_get_followers.py
|
|
│ │ ├── player_get_founded_clan.py
|
|
│ │ ├── player_get_participating_events.py
|
|
│ │ ├── player_get_patreon.py
|
|
│ │ ├── player_get_players.py
|
|
│ │ ├── player_get.py
|
|
│ │ └── player_get_ranked_maps.py
|
|
│ ├── player_scores
|
|
│ │ ├── __init__.py
|
|
│ │ ├── player_scores_acc_graph.py
|
|
│ │ ├── player_scores_get_compact_history.py
|
|
│ │ ├── player_scores_get_compact_scores.py
|
|
│ │ ├── player_scores_get_history.py
|
|
│ │ ├── player_scores_get_pinned_scores.py
|
|
│ │ ├── player_scores_get_scores.py
|
|
│ │ └── player_scores_get_score_value.py
|
|
│ ├── song
|
|
│ │ ├── __init__.py
|
|
│ │ └── song_get_all.py
|
|
│ └── __init__.py
|
|
├── models
|
|
│ ├── __pycache__
|
|
│ ├── achievement_description.py
|
|
│ ├── achievement_level.py
|
|
│ ├── achievement.py
|
|
│ ├── badge.py
|
|
│ ├── ban.py
|
|
│ ├── beasties_nomination.py
|
|
│ ├── besties_nomination_response.py
|
|
│ ├── clan_bigger_response.py
|
|
│ ├── clan_global_map_point.py
|
|
│ ├── clan_global_map.py
|
|
│ ├── clan_map_connection.py
|
|
│ ├── clan_maps_sort_by.py
|
|
│ ├── clan_point.py
|
|
│ ├── clan.py
|
|
│ ├── clan_ranking_response_clan_response_full_response_with_metadata_and_container.py
|
|
│ ├── clan_ranking_response.py
|
|
│ ├── clan_response_full.py
|
|
│ ├── clan_response_full_response_with_metadata.py
|
|
│ ├── clan_response.py
|
|
│ ├── clan_sort_by.py
|
|
│ ├── compact_leaderboard.py
|
|
│ ├── compact_leaderboard_response.py
|
|
│ ├── compact_score.py
|
|
│ ├── compact_score_response.py
|
|
│ ├── compact_score_response_response_with_metadata.py
|
|
│ ├── compact_song_response.py
|
|
│ ├── controller_enum.py
|
|
│ ├── criteria_commentary.py
|
|
│ ├── difficulty_description.py
|
|
│ ├── difficulty_response.py
|
|
│ ├── difficulty_status.py
|
|
│ ├── event_player.py
|
|
│ ├── event_ranking.py
|
|
│ ├── external_status.py
|
|
│ ├── featured_playlist.py
|
|
│ ├── featured_playlist_response.py
|
|
│ ├── follower_type.py
|
|
│ ├── global_map_history.py
|
|
│ ├── history_compact_response.py
|
|
│ ├── hmd.py
|
|
│ ├── info_to_highlight.py
|
|
│ ├── __init__.py
|
|
│ ├── leaderboard_change.py
|
|
│ ├── leaderboard_clan_ranking_response.py
|
|
│ ├── leaderboard_contexts.py
|
|
│ ├── leaderboard_group_entry.py
|
|
│ ├── leaderboard_info_response.py
|
|
│ ├── leaderboard_info_response_response_with_metadata.py
|
|
│ ├── leaderboard.py
|
|
│ ├── leaderboard_response.py
|
|
│ ├── leaderboard_sort_by.py
|
|
│ ├── legacy_modifiers.py
|
|
│ ├── link_response.py
|
|
│ ├── map_diff_response.py
|
|
│ ├── map_info_response.py
|
|
│ ├── map_info_response_response_with_metadata.py
|
|
│ ├── mapper.py
|
|
│ ├── mapper_response.py
|
|
│ ├── map_quality.py
|
|
│ ├── map_sort_by.py
|
|
│ ├── maps_type.py
|
|
│ ├── metadata.py
|
|
│ ├── modifiers_map.py
|
|
│ ├── modifiers_rating.py
|
|
│ ├── my_type.py
|
|
│ ├── operation.py
|
|
│ ├── order.py
|
|
│ ├── participating_event_response.py
|
|
│ ├── patreon_features.py
|
|
│ ├── player_change.py
|
|
│ ├── player_context_extension.py
|
|
│ ├── player_follower.py
|
|
│ ├── player_followers_info_response.py
|
|
│ ├── player.py
|
|
│ ├── player_response_clan_response_full_response_with_metadata_and_container.py
|
|
│ ├── player_response_full.py
|
|
│ ├── player_response.py
|
|
│ ├── player_response_with_stats.py
|
|
│ ├── player_response_with_stats_response_with_metadata.py
|
|
│ ├── player_score_stats_history.py
|
|
│ ├── player_score_stats.py
|
|
│ ├── player_search.py
|
|
│ ├── player_social.py
|
|
│ ├── player_sort_by.py
|
|
│ ├── pp_type.py
|
|
│ ├── profile_settings.py
|
|
│ ├── qualification_change.py
|
|
│ ├── qualification_commentary.py
|
|
│ ├── qualification_vote.py
|
|
│ ├── ranked_mapper_response.py
|
|
│ ├── ranked_map.py
|
|
│ ├── rank_qualification.py
|
|
│ ├── rank_update_change.py
|
|
│ ├── rank_update.py
|
|
│ ├── rank_voting.py
|
|
│ ├── replay_offsets.py
|
|
│ ├── requirements.py
|
|
│ ├── score_filter_status.py
|
|
│ ├── score_graph_entry.py
|
|
│ ├── score_improvement.py
|
|
│ ├── score_metadata.py
|
|
│ ├── score_response.py
|
|
│ ├── score_response_with_acc.py
|
|
│ ├── score_response_with_my_score.py
|
|
│ ├── score_response_with_my_score_response_with_metadata.py
|
|
│ ├── scores_sort_by.py
|
|
│ ├── song.py
|
|
│ ├── song_response.py
|
|
│ ├── song_status.py
|
|
│ ├── type.py
|
|
│ └── voter_feedback.py
|
|
├── __pycache__
|
|
├── client.py
|
|
├── errors.py
|
|
├── __init__.py
|
|
├── py.typed
|
|
└── types.py
|
|
|
|
13 directories, 158 files
|
|
```
|
|
|
|
Here's the contents of `src/clients/beatleader/client.py`:
|
|
|
|
```python
|
|
import ssl
|
|
from typing import Any, Dict, Optional, Union
|
|
|
|
import httpx
|
|
from attrs import define, evolve, field
|
|
|
|
|
|
@define
|
|
class Client:
|
|
"""A class for keeping track of data related to the API
|
|
|
|
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
|
|
|
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
|
|
|
``cookies``: A dictionary of cookies to be sent with every request
|
|
|
|
``headers``: A dictionary of headers to be sent with every request
|
|
|
|
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
|
httpx.TimeoutException if this is exceeded.
|
|
|
|
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
|
but can be set to False for testing purposes.
|
|
|
|
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
|
|
|
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
|
|
|
|
|
Attributes:
|
|
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
|
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
|
argument to the constructor.
|
|
"""
|
|
|
|
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
|
_base_url: str = field(alias="base_url")
|
|
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
|
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
|
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
|
|
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
|
|
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
|
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
|
_client: Optional[httpx.Client] = field(default=None, init=False)
|
|
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
|
|
|
|
def with_headers(self, headers: Dict[str, str]) -> "Client":
|
|
"""Get a new client matching this one with additional headers"""
|
|
if self._client is not None:
|
|
self._client.headers.update(headers)
|
|
if self._async_client is not None:
|
|
self._async_client.headers.update(headers)
|
|
return evolve(self, headers={**self._headers, **headers})
|
|
|
|
def with_cookies(self, cookies: Dict[str, str]) -> "Client":
|
|
"""Get a new client matching this one with additional cookies"""
|
|
if self._client is not None:
|
|
self._client.cookies.update(cookies)
|
|
if self._async_client is not None:
|
|
self._async_client.cookies.update(cookies)
|
|
return evolve(self, cookies={**self._cookies, **cookies})
|
|
|
|
def with_timeout(self, timeout: httpx.Timeout) -> "Client":
|
|
"""Get a new client matching this one with a new timeout (in seconds)"""
|
|
if self._client is not None:
|
|
self._client.timeout = timeout
|
|
if self._async_client is not None:
|
|
self._async_client.timeout = timeout
|
|
return evolve(self, timeout=timeout)
|
|
|
|
def set_httpx_client(self, client: httpx.Client) -> "Client":
|
|
"""Manually the underlying httpx.Client
|
|
|
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
"""
|
|
self._client = client
|
|
return self
|
|
|
|
def get_httpx_client(self) -> httpx.Client:
|
|
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
|
if self._client is None:
|
|
self._client = httpx.Client(
|
|
base_url=self._base_url,
|
|
cookies=self._cookies,
|
|
headers=self._headers,
|
|
timeout=self._timeout,
|
|
verify=self._verify_ssl,
|
|
follow_redirects=self._follow_redirects,
|
|
**self._httpx_args,
|
|
)
|
|
return self._client
|
|
|
|
def __enter__(self) -> "Client":
|
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
|
self.get_httpx_client().__enter__()
|
|
return self
|
|
|
|
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
|
|
|
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
|
|
"""Manually the underlying httpx.AsyncClient
|
|
|
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
"""
|
|
self._async_client = async_client
|
|
return self
|
|
|
|
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
|
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
|
if self._async_client is None:
|
|
self._async_client = httpx.AsyncClient(
|
|
base_url=self._base_url,
|
|
cookies=self._cookies,
|
|
headers=self._headers,
|
|
timeout=self._timeout,
|
|
verify=self._verify_ssl,
|
|
follow_redirects=self._follow_redirects,
|
|
**self._httpx_args,
|
|
)
|
|
return self._async_client
|
|
|
|
async def __aenter__(self) -> "Client":
|
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
|
await self.get_async_httpx_client().__aenter__()
|
|
return self
|
|
|
|
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
|
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
|
|
|
|
|
@define
|
|
class AuthenticatedClient:
|
|
"""A Client which has been authenticated for use on secured endpoints
|
|
|
|
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
|
|
|
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
|
|
|
``cookies``: A dictionary of cookies to be sent with every request
|
|
|
|
``headers``: A dictionary of headers to be sent with every request
|
|
|
|
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
|
httpx.TimeoutException if this is exceeded.
|
|
|
|
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
|
but can be set to False for testing purposes.
|
|
|
|
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
|
|
|
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
|
|
|
|
|
Attributes:
|
|
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
|
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
|
argument to the constructor.
|
|
token: The token to use for authentication
|
|
prefix: The prefix to use for the Authorization header
|
|
auth_header_name: The name of the Authorization header
|
|
"""
|
|
|
|
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
|
_base_url: str = field(alias="base_url")
|
|
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
|
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
|
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
|
|
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
|
|
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
|
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
|
_client: Optional[httpx.Client] = field(default=None, init=False)
|
|
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
|
|
|
|
token: str
|
|
prefix: str = "Bearer"
|
|
auth_header_name: str = "Authorization"
|
|
|
|
def with_headers(self, headers: Dict[str, str]) -> "AuthenticatedClient":
|
|
"""Get a new client matching this one with additional headers"""
|
|
if self._client is not None:
|
|
self._client.headers.update(headers)
|
|
if self._async_client is not None:
|
|
self._async_client.headers.update(headers)
|
|
return evolve(self, headers={**self._headers, **headers})
|
|
|
|
def with_cookies(self, cookies: Dict[str, str]) -> "AuthenticatedClient":
|
|
"""Get a new client matching this one with additional cookies"""
|
|
if self._client is not None:
|
|
self._client.cookies.update(cookies)
|
|
if self._async_client is not None:
|
|
self._async_client.cookies.update(cookies)
|
|
return evolve(self, cookies={**self._cookies, **cookies})
|
|
|
|
def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
|
|
"""Get a new client matching this one with a new timeout (in seconds)"""
|
|
if self._client is not None:
|
|
self._client.timeout = timeout
|
|
if self._async_client is not None:
|
|
self._async_client.timeout = timeout
|
|
return evolve(self, timeout=timeout)
|
|
|
|
def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
|
|
"""Manually the underlying httpx.Client
|
|
|
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
"""
|
|
self._client = client
|
|
return self
|
|
|
|
def get_httpx_client(self) -> httpx.Client:
|
|
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
|
if self._client is None:
|
|
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
|
self._client = httpx.Client(
|
|
base_url=self._base_url,
|
|
cookies=self._cookies,
|
|
headers=self._headers,
|
|
timeout=self._timeout,
|
|
verify=self._verify_ssl,
|
|
follow_redirects=self._follow_redirects,
|
|
**self._httpx_args,
|
|
)
|
|
return self._client
|
|
|
|
def __enter__(self) -> "AuthenticatedClient":
|
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
|
self.get_httpx_client().__enter__()
|
|
return self
|
|
|
|
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
|
|
|
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
|
|
"""Manually the underlying httpx.AsyncClient
|
|
|
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
"""
|
|
self._async_client = async_client
|
|
return self
|
|
|
|
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
|
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
|
if self._async_client is None:
|
|
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
|
self._async_client = httpx.AsyncClient(
|
|
base_url=self._base_url,
|
|
cookies=self._cookies,
|
|
headers=self._headers,
|
|
timeout=self._timeout,
|
|
verify=self._verify_ssl,
|
|
follow_redirects=self._follow_redirects,
|
|
**self._httpx_args,
|
|
)
|
|
return self._async_client
|
|
|
|
async def __aenter__(self) -> "AuthenticatedClient":
|
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
|
await self.get_async_httpx_client().__aenter__()
|
|
return self
|
|
|
|
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
|
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
|
|
|
```
|
|
|
|
Here is our attempt at using this client in ipython:
|
|
|
|
```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
|
|
)
|
|
|
|
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(
|
|
client=client,
|
|
id=player_id)
|
|
```
|
|
|
|
And the result:
|
|
|
|
```
|
|
---------------------------------------------------------------------------
|
|
AttributeError Traceback (most recent call last)
|
|
Cell In[1], line 21
|
|
17 player_id = '76561199407393962'
|
|
19 BASE_URL = "https://api.beatleader.xyz"
|
|
---> 21 response: ScoreResponseWithMyScoreResponseWithMetadata = player_scores_get_compact_scores.sync_detailed(
|
|
22 client=beatleader_client,
|
|
23 id=player_id)
|
|
|
|
File ~/ops/beatsaber/playlist-tool/src/clients/beatleader/api/player_scores/player_scores_get_compact_scores.py:216, in sync_detailed(id, client, sort_by, order, page, count, search, diff, mode, requirements, score_status, leaderboard_context, type, modifiers, stars_from, stars_to, time_from, time_to, event_id)
|
|
162 """Retrieve player's scores in a compact form
|
|
163
|
|
164 Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
|
(...)
|
|
192 Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
|
|
193 """
|
|
195 kwargs = _get_kwargs(
|
|
196 id=id,
|
|
197 sort_by=sort_by,
|
|
(...)
|
|
213 event_id=event_id,
|
|
214 )
|
|
--> 216 response = client.get_httpx_client().request(
|
|
217 **kwargs,
|
|
218 )
|
|
220 return _build_response(client=client, response=response)
|
|
|
|
AttributeError: module 'clients.beatleader.client' has no attribute 'get_httpx_client'
|
|
|
|
```
|